AppSearch es una solución de búsqueda integrada en el dispositivo de alto rendimiento para administrar datos estructurados almacenados de forma local. Contiene APIs para indexar y recuperar datos con la búsqueda en el texto completo. Las aplicaciones pueden usar AppSearch para ofrecer funciones de búsqueda personalizadas en la app, lo que permite a los usuarios buscar contenido incluso sin conexión.
AppSearch ofrece las siguientes funciones:
- Una implementación de almacenamiento rápida y centrada en dispositivos móviles con un uso bajo de E/S
- Indexación y consultas altamente eficientes en grandes conjuntos de datos
- Compatibilidad con varios idiomas, como inglés y español
- Clasificación de relevancia y puntuación de uso
Debido al menor uso de E/S, AppSearch ofrece una latencia más baja para la indexación y la búsqueda en conjuntos de datos grandes en comparación con SQLite. AppSearch simplifica las consultas de varios tipos, ya que admite consultas únicas, mientras que SQLite combina los resultados de varias tablas.
Para ilustrar las funciones de AppSearch, tomemos el ejemplo de una aplicación de música que administra las canciones favoritas de los usuarios y les permite buscarlas con facilidad. Los usuarios disfrutan de música de todo el mundo con títulos de canciones en diferentes idiomas, para los que AppSearch admite de forma nativa la indexación y las consultas. Cuando el usuario busca una canción por título o nombre del artista, la aplicación simplemente pasa la solicitud a AppSearch para recuperar las canciones que coinciden de forma rápida y eficiente. La aplicación muestra los resultados, lo que permite que los usuarios comiencen a reproducir sus canciones favoritas rápidamente.
Configuración
Para usar AppSearch en tu aplicación, agrega las siguientes dependencias al archivo build.gradle de la aplicación:
Groovy
dependencies { def appsearch_version = "1.2.0-alpha02" implementation "androidx.appsearch:appsearch:$appsearch_version" // Use kapt instead of annotationProcessor if writing Kotlin classes annotationProcessor "androidx.appsearch:appsearch-compiler:$appsearch_version" implementation "androidx.appsearch:appsearch-local-storage:$appsearch_version" // PlatformStorage is compatible with Android 12+ devices and supports sharing data // with other apps. Feature support varies by device. Before calling an API that // requires a feature, check Features.isFeatureSupported(). implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version" // PlayServicesStorage is compatible with all devices that support Google Play Services on // all API levels. It offers similar capabilities to PlatformStorage and is the // recommended solution for lower API levels on which PlatformStorage is not supported. implementation "androidx.appsearch:appsearch-play-services-storage:$appsearch_version" }
Kotlin
dependencies { val appsearch_version = "1.2.0-alpha02" implementation("androidx.appsearch:appsearch:$appsearch_version") // Use annotationProcessor instead of kapt if writing Java classes kapt("androidx.appsearch:appsearch-compiler:$appsearch_version") implementation("androidx.appsearch:appsearch-local-storage:$appsearch_version") // PlatformStorage is compatible with Android 12+ devices and supports sharing data // with other apps. Feature support varies by device. Before calling an API that // requires a feature, check Features.isFeatureSupported(). implementation("androidx.appsearch:appsearch-platform-storage:$appsearch_version") // PlayServicesStorage is compatible with all devices that support Google Play Services on // all API levels. It offers similar capabilities to PlatformStorage and is the // recommended solution for lower API levels on which PlatformStorage is not supported. implementation("androidx.appsearch:appsearch-play-services-storage:$appsearch_version") }
Conceptos de AppSearch
En el siguiente diagrama, se ilustran los conceptos de AppSearch y sus interacciones.
Figura 1: Diagrama de los conceptos de AppSearch: base de datos de AppSearch, esquema,
tipos de esquemas, documentos, sesión y búsqueda.
Base de datos y sesión
Una base de datos de AppSearch es una colección de documentos que se ajusta al esquema de la base de datos. Las aplicaciones cliente crean una base de datos proporcionando su contexto de aplicación y un nombre de base de datos. Solo la aplicación que las creó puede abrir las bases de datos. Cuando se abre una base de datos, se devuelve una sesión para interactuar con ella. La sesión es el punto de entrada para llamar a las APIs de AppSearch y permanece abierta hasta que la aplicación cliente la cierra.
Esquema y tipos de esquemas
Un esquema representa la estructura organizativa de los datos dentro de una base de datos de AppSearch.
El esquema se compone de tipos de esquemas que representan tipos de datos únicos. Los tipos de esquemas constan de propiedades que contienen un nombre, un tipo de datos y una cardinalidad. Una vez que se agrega un tipo de esquema al esquema de la base de datos, se pueden crear documentos de ese tipo de esquema y agregarlos a la base de datos.
Documentos
En AppSearch, una unidad de datos se representa como un documento. Cada documento de una base de datos de AppSearch se identifica de forma única por su espacio de nombres y su ID. Los espacios de nombres se usan para separar los datos de diferentes fuentes cuando solo se necesita consultar una fuente, como las cuentas de usuario.
Los documentos contienen una marca de tiempo de creación, un tiempo de actividad (TTL) y una puntuación que se puede usar para la clasificación durante la recuperación. A un documento también se le asigna un tipo de esquema que describe las propiedades de datos adicionales que debe tener el documento.
Una clase de documento es una abstracción de un documento. Contiene campos anotados que representan el contenido de un documento. De forma predeterminada, el nombre de la clase de documento establece el nombre del tipo de esquema.
Buscar
Los documentos se indexan y se pueden buscar proporcionando una consulta. Un documento se coincide y se incluye en los resultados de la búsqueda si contiene los términos de la consulta o coincide con otra especificación de búsqueda. Los resultados se ordenan según su puntuación y estrategia de clasificación. Los resultados de la búsqueda se representan con páginas que puedes recuperar de forma secuencial.
AppSearch ofrece personalizaciones para la búsqueda, como filtros, configuración del tamaño de la página y fragmentos.
Almacenamiento de la plataforma, almacenamiento local o almacenamiento de los Servicios de Play
AppSearch ofrece tres soluciones de almacenamiento: LocalStorage, PlatformStorage y PlayServicesStorage. Con LocalStorage, tu aplicación administra un índice específico de la app que se encuentra en el directorio de datos de la aplicación. Con PlatformStorage y PlayServicesStorage, tu aplicación contribuye a un índice central de todo el sistema. El índice de PlatformStorage se aloja en el servidor del sistema, y el de PlayServicesStorage, en el almacenamiento de los Servicios de Google Play. El acceso a los datos dentro de estos índices centrales se restringe a los datos que tu
aplicación aportó y a los que otra aplicación compartió contigo de forma explícita. Todas estas opciones de almacenamiento comparten la misma API y se pueden intercambiar según la versión del dispositivo.
Ten en cuenta que la compatibilidad con las funciones varía según la opción de almacenamiento y el dispositivo. Para obtener más información, consulta Cómo verificar la disponibilidad de funciones.
El siguiente código crea una sesión de PlatformStorage en Android 12 (nivel de API 31) y versiones posteriores, y una sesión de PlayServicesStorage o LocalStorage en versiones anteriores:
Kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { appSearchSessionFuture.setFuture( PlatformStorage.createSearchSession( PlatformStorage.SearchContext.Builder(mContext, DATABASE_NAME) .build() ) ) } else { if (usePlayServicesStorageBelowS) { appSearchSessionFuture.setFuture( PlayServicesStorage.createSearchSession( PlayServicesStorage.SearchContext.Builder(mContext, DATABASE_NAME) .build() ) ) } else { appSearchSessionFuture.setFuture( LocalStorage.createSearchSession( LocalStorage.SearchContext.Builder(mContext, DATABASE_NAME) .build() ) ) } }
Java
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSession( new PlatformStorage.SearchContext.Builder(mContext, DATABASE_NAME) .build())); } else { if (usePlayServicesStorageBelowS) { mAppSearchSessionFuture.setFuture(PlayServicesStorage.createSearchSession( new PlayServicesStorage.SearchContext.Builder(mContext, DATABASE_NAME) .build())); } else { mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession( new LocalStorage.SearchContext.Builder(mContext, DATABASE_NAME) .build())); } }
Con PlatformStorage y PlayServicesStorage, tu aplicación puede compartir datos de forma segura con otras aplicaciones para permitirles buscar también en los datos de tu app. El uso compartido de datos de la aplicación de solo lectura se otorga a través de un protocolo de enlace de certificados para garantizar que la otra aplicación tenga permiso para leer los datos. Obtén más información sobre esta API en la documentación de setSchemaTypeVisibilityForPackage().
Además, con PlatformStorage, los datos que se indexan se pueden mostrar en las plataformas de la IU del sistema. Las aplicaciones pueden inhabilitar que se muestren algunos o todos sus datos en las plataformas de la IU del sistema. Obtén más información sobre esta API en la
documentación de setSchemaTypeDisplayedBySystem().
| Funciones | LocalStorage (compatible con Android 5.0 y versiones posteriores) |
PlatformStorage (compatible con Android 12 y versiones posteriores) |
PlayServicesStorage (compatible con Android 5.0 y versiones posteriores) |
|---|---|---|---|
| Búsqueda en el texto completo eficiente | |||
| Compatibilidad con diferentes idiomas | |||
| Se redujo el tamaño de los objetos binarios. | |||
| Uso compartido de datos entre aplicaciones | |||
| Capacidad de mostrar datos en las plataformas de la IU del sistema | |||
| Se puede indexar una cantidad y un tamaño de documentos ilimitados | |||
| Operaciones más rápidas sin latencia adicional de Binder |
Existen compensaciones adicionales que debes tener en cuenta cuando elijas entre LocalStorage
y PlatformStorage. Como PlatformStorage une las APIs de Jetpack al servicio del sistema de AppSearch, el impacto en el tamaño del APK es mínimo en comparación con el uso de LocalStorage. Sin embargo, esto también significa que las operaciones de AppSearch generan una latencia de Binder adicional cuando se llama al servicio del sistema de AppSearch. Con PlatformStorage, AppSearch limita la cantidad y el tamaño de los documentos que una aplicación puede indexar para garantizar un índice central eficiente. PlayServicesStorage también tiene
las mismas limitaciones que PlatformStorage y solo se admite en dispositivos con
Servicios de Google Play.
Revisa la disponibilidad de las funciones
Si bien todas las opciones de almacenamiento comparten la misma API, algunas funciones no están disponibles con todas las opciones de almacenamiento ni en todos los dispositivos. En la referencia de la API, la anotación @RequiresFeature identifica las APIs que requieren una de estas funciones, y la clase Features define una constante para cada función. Por ejemplo,
INDEXING_TYPE_RANGE
requiere Features.NUMERIC_SEARCH.
En el caso de PlatformStorage, la compatibilidad con las funciones depende de la versión de Android del dispositivo.
A partir de Android 13 (nivel de API 33), la compatibilidad con las funciones también depende de las actualizaciones del sistema de Google Play que haya recibido el dispositivo. Por ejemplo,
PlatformStorage admite la búsqueda numérica (Features.NUMERIC_SEARCH), la búsqueda
textual (Features.VERBATIM_SEARCH) y las uniones
(Features.JOIN_SPEC_AND_QUALIFIED_ID) solo en los siguientes dispositivos:
- Dispositivos que ejecutan Android 14 (nivel de API 34) o versiones posteriores
- Dispositivos que ejecutan Android 13 (nivel de API 33) y que recibieron la actualización del sistema de Google Play requerida
Estas funciones no son compatibles con dispositivos que ejecutan Android 12 o Android 12L (niveles de API 31 y 32).
La cantidad máxima de propiedades indexadas en un documento también varía según el dispositivo
y la opción de almacenamiento. Para obtener el límite durante el tiempo de ejecución, llama a getMaxIndexedProperties().
No te bases solo en la versión de Android para determinar la compatibilidad con las funciones. Antes de llamar a una API que requiera una función, verifica que la función sea compatible llamando a isFeatureSupported() en el objeto Features que muestra AppSearchSession.getFeatures(). Si no se admite una función obligatoria, la operación falla con una
excepción. Un ejemplo de caso de error es establecer un esquema que incluya una propiedad INDEXING_TYPE_RANGE que falle en dispositivos que no admitan Features.NUMERIC_SEARCH.
En el siguiente código, se verifica si un AppSearchSession abierto admite la búsqueda numérica:
Kotlin
val features = session.features if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) { // Numeric search is supported. You can index properties with // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with // SearchSpec.Builder.setNumericSearchEnabled(true). } else { // Numeric search isn't supported. Set a schema without range-indexed // properties, or switch to a different storage option. }
Java
Features features = session.getFeatures(); if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) { // Numeric search is supported. You can index properties with // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with // SearchSpec.Builder.setNumericSearchEnabled(true). } else { // Numeric search isn't supported. Set a schema without range-indexed // properties, or switch to a different storage option. }
Comienza a usar AppSearch
En el ejemplo de esta sección, se muestra cómo usar las APIs de AppSearch para integrarlas en una aplicación hipotética de toma de notas.
Escribe una clase de documento
El primer paso para realizar la integración con AppSearch es escribir una clase de documento que describa los datos que se insertarán en la base de datos. Marca una clase como una clase de documento
con la anotación @Document.Puedes usar instancias de la clase de documento para colocar documentos en la base de datos y
recuperarlos de ella.
En el siguiente código, se define una clase de documento Note con un campo anotado @Document.StringProperty para indexar el texto de un objeto Note.
Kotlin
@Document public data class Note( // Required field for a document class. All documents MUST have a namespace. @Document.Namespace val namespace: String, // Required field for a document class. All documents MUST have an Id. @Document.Id val id: String, // Optional field for a document class, used to set the score of the // document. If this is not included in a document class, the score is set // to a default of 0. @Document.Score val score: Int, // Optional field for a document class, used to index a note's text for this // document class. @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES) val text: String )
Java
@Document public class Note { // Required field for a document class. All documents MUST have a namespace. @Document.Namespace private final String namespace; // Required field for a document class. All documents MUST have an Id. @Document.Id private final String id; // Optional field for a document class, used to set the score of the // document. If this is not included in a document class, the score is set // to a default of 0. @Document.Score private final int score; // Optional field for a document class, used to index a note's text for this // document class. @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES) private final String text; Note(@NonNull String id, @NonNull String namespace, int score, @NonNull String text) { this.id = Objects.requireNonNull(id); this.namespace = Objects.requireNonNull(namespace); this.score = score; this.text = Objects.requireNonNull(text); } @NonNull public String getNamespace() { return namespace; } @NonNull public String getId() { return id; } public int getScore() { return score; } @NonNull public String getText() { return text; } }
Abre una base de datos
Debes crear una base de datos antes de trabajar con documentos. El siguiente código crea una base de datos nueva con el nombre notes_app y obtiene un ListenableFuture para un AppSearchSession, que representa la conexión a la base de datos y proporciona las APIs para las operaciones de la base de datos.
Kotlin
val context: Context = getApplicationContext() val sessionFuture = LocalStorage.createSearchSession( LocalStorage.SearchContext.Builder(context, /*databaseName=*/"notes_app") .build() )
Java
Context context = getApplicationContext(); ListenableFuture<AppSearchSession> sessionFuture = LocalStorage.createSearchSession( new LocalStorage.SearchContext.Builder(context, /*databaseName=*/ "notes_app") .build() );
Establece un esquema
Debes establecer un esquema antes de poder colocar documentos en la base de datos y recuperarlos de ella. El esquema de la base de datos consta de diferentes tipos de datos estructurados, denominados "tipos de esquema". En el siguiente código, se establece el esquema proporcionando la clase de documento como un tipo de esquema.
Kotlin
val setSchemaRequest = SetSchemaRequest.Builder().addDocumentClasses(Note::class.java) .build() val setSchemaFuture = Futures.transformAsync( sessionFuture, { session -> session?.setSchema(setSchemaRequest) }, mExecutor )
Java
SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().addDocumentClasses(Note.class) .build(); ListenableFuture<SetSchemaResponse> setSchemaFuture = Futures.transformAsync(sessionFuture, session -> session.setSchema(setSchemaRequest), mExecutor);
Coloca un documento en la base de datos
Una vez que se agrega un tipo de esquema, puedes agregar documentos de ese tipo a la base de datos.
En el siguiente código, se compila un documento del tipo de esquema Note con el compilador de clases de documentos Note. Establece el espacio de nombres del documento user1 para representar a un
usuario arbitrario de este ejemplo. Luego, el documento se inserta en la base de datos
y se adjunta un objeto de escucha para procesar el resultado de la operación de inserción.
Kotlin
val note = Note( namespace="user1", id="noteId", score=10, text="Buy fresh fruit" ) val putRequest = PutDocumentsRequest.Builder().addDocuments(note).build() val putFuture = Futures.transformAsync( sessionFuture, { session -> session?.put(putRequest) }, mExecutor ) Futures.addCallback( putFuture, object : FutureCallback<AppSearchBatchResult<String, Void>?> { override fun onSuccess(result: AppSearchBatchResult<String, Void>?) { // Gets map of successful results from Id to Void val successfulResults = result?.successes // Gets map of failed results from Id to AppSearchResult val failedResults = result?.failures } override fun onFailure(t: Throwable) { Log.e(TAG, "Failed to put documents.", t) } }, mExecutor )
Java
Note note = new Note(/*namespace=*/"user1", /*id=*/ "noteId", /*score=*/ 10, /*text=*/ "Buy fresh fruit!"); PutDocumentsRequest putRequest = new PutDocumentsRequest.Builder().addDocuments(note) .build(); ListenableFuture<AppSearchBatchResult<String, Void>> putFuture = Futures.transformAsync(sessionFuture, session -> session.put(putRequest), mExecutor); Futures.addCallback(putFuture, new FutureCallback<AppSearchBatchResult<String, Void>>() { @Override public void onSuccess(@Nullable AppSearchBatchResult<String, Void> result) { // Gets map of successful results from Id to Void Map<String, Void> successfulResults = result.getSuccesses(); // Gets map of failed results from Id to AppSearchResult Map<String, AppSearchResult<Void>> failedResults = result.getFailures(); } @Override public void onFailure(@NonNull Throwable t) { Log.e(TAG, "Failed to put documents.", t); } }, mExecutor);
Buscar
Puedes buscar documentos que se indexen con las operaciones de búsqueda que se describen en
esta sección. El siguiente código realiza consultas para el término "fruit" en la
base de datos de documentos que pertenecen al espacio de nombres user1.
Kotlin
val searchSpec = SearchSpec.Builder() .addFilterNamespaces("user1") .build(); val searchFuture = Futures.transform( sessionFuture, { session -> session?.search("fruit", searchSpec) }, mExecutor ) Futures.addCallback( searchFuture, object : FutureCallback<SearchResults> { override fun onSuccess(searchResults: SearchResults?) { iterateSearchResults(searchResults) } override fun onFailure(t: Throwable?) { Log.e("TAG", "Failed to search notes in AppSearch.", t) } }, mExecutor )
Java
SearchSpec searchSpec = new SearchSpec.Builder() .addFilterNamespaces("user1") .build(); ListenableFuture<SearchResults> searchFuture = Futures.transform(sessionFuture, session -> session.search("fruit", searchSpec), mExecutor); Futures.addCallback(searchFuture, new FutureCallback<SearchResults>() { @Override public void onSuccess(@Nullable SearchResults searchResults) { iterateSearchResults(searchResults); } @Override public void onFailure(@NonNull Throwable t) { Log.e(TAG, "Failed to search notes in AppSearch.", t); } }, mExecutor);
Itera a través de SearchResults
Las búsquedas devuelven una instancia de SearchResults, que otorga acceso a las páginas de objetos SearchResult. Cada SearchResult
contiene su GenericDocument coincidente, la forma general de un
documento al que se convierten todos los documentos. El siguiente código obtiene la primera
página de resultados de la búsqueda y vuelve a convertir el resultado en un documento Note.
Kotlin
Futures.transform( searchResults?.nextPage, { page: List<SearchResult>? -> // Gets GenericDocument from SearchResult. val genericDocument: GenericDocument = page!![0].genericDocument val schemaType = genericDocument.schemaType val note: Note? = try { if (schemaType == "Note") { // Converts GenericDocument object to Note object. genericDocument.toDocumentClass(Note::class.java) } else null } catch (e: AppSearchException) { Log.e( TAG, "Failed to convert GenericDocument to Note", e ) null } note }, mExecutor )
Java
Futures.transform(searchResults.getNextPage(), page -> { // Gets GenericDocument from SearchResult. GenericDocument genericDocument = page.get(0).getGenericDocument(); String schemaType = genericDocument.getSchemaType(); Note note = null; if (schemaType.equals("Note")) { try { // Converts GenericDocument object to Note object. note = genericDocument.toDocumentClass(Note.class); } catch (AppSearchException e) { Log.e(TAG, "Failed to convert GenericDocument to Note", e); } } return note; }, mExecutor);
Quita un documento
Cuando el usuario borra una nota, la aplicación borra el documento Note correspondiente de la base de datos. Esto garantiza que la nota ya no aparecerá en las
consultas. Con el siguiente código, se realiza una solicitud explícita para quitar el documento Note
de la base de datos por ID.
Kotlin
val removeRequest = RemoveByDocumentIdRequest.Builder("user1") .addIds("noteId") .build() val removeFuture = Futures.transformAsync( sessionFuture, { session -> session?.remove(removeRequest) }, mExecutor )
Java
RemoveByDocumentIdRequest removeRequest = new RemoveByDocumentIdRequest.Builder("user1") .addIds("noteId") .build(); ListenableFuture<AppSearchBatchResult<String, Void>> removeFuture = Futures.transformAsync(sessionFuture, session -> session.remove(removeRequest), mExecutor);
Persistir en el disco
Las actualizaciones de una base de datos se deben conservar periódicamente en el disco llamando a requestFlush(). En el siguiente código, se llama a requestFlush() con un objeto de escucha para determinar si la llamada se realizó correctamente.
Kotlin
val requestFlushFuture = Futures.transformAsync( sessionFuture, { session -> session?.requestFlush() }, mExecutor ) Futures.addCallback(requestFlushFuture, object : FutureCallback<Void?> { override fun onSuccess(result: Void?) { // Success! Database updates have been persisted to disk. } override fun onFailure(t: Throwable) { Log.e(TAG, "Failed to flush database updates.", t) } }, mExecutor)
Java
ListenableFuture<Void> requestFlushFuture = Futures.transformAsync(sessionFuture, session -> session.requestFlush(), mExecutor); Futures.addCallback(requestFlushFuture, new FutureCallback<Void>() { @Override public void onSuccess(@Nullable Void result) { // Success! Database updates have been persisted to disk. } @Override public void onFailure(@NonNull Throwable t) { Log.e(TAG, "Failed to flush database updates.", t); } }, mExecutor);
Cómo cerrar una sesión
Se debe cerrar un objeto AppSearchSession
cuando una aplicación ya no llame a ninguna operación de la base de
datos. El siguiente código cierra la sesión de AppSearch que se abrió antes y conserva todas las actualizaciones en el disco.
Kotlin
val closeFuture = Futures.transform<AppSearchSession, Unit>(sessionFuture, { session -> session?.close() Unit }, mExecutor )
Java
ListenableFuture<Void> closeFuture = Futures.transform(sessionFuture, session -> { session.close(); return null; }, mExecutor);
Recursos adicionales
Para obtener más información sobre AppSearch, consulta los siguientes recursos adicionales:
Ejemplos
- Muestra de AppSearch para Android (Kotlin): Es una app para tomar notas que usa AppSearch para indexar las notas de un usuario y permitirle buscar en ellas.
Enviar comentarios
Comparte tus comentarios e ideas con nosotros por medio de estos recursos:
Informa los errores para que podamos corregirlos.