AppSearch

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 datos y recuperarlos 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.

Diagrama en el que se ilustra la indexación y la búsqueda dentro de AppSearch

AppSearch proporciona las siguientes funciones:

  • Una implementación de almacenamiento rápida y centrada en los dispositivos móviles con bajo uso 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 indexar y buscar en conjuntos de datos grandes en comparación con SQLite. AppSearch simplifica las consultas entre 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 fácilmente. Los usuarios disfrutan de música de todo el mundo con títulos de canciones en diferentes idiomas, que AppSearch admite de forma nativa para la indexación y las consultas. Cuando el usuario busca una canción por título o nombre de artista, la aplicación simplemente pasa la solicitud a AppSearch para recuperar canciones coincidentes de forma rápida y eficiente. La aplicación muestra los resultados, lo que permite a los usuarios comenzar 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.1.0-alpha05"

    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 offers additional features
    // to LocalStorage.
    implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version"
}

Kotlin

dependencies {
    val appsearch_version = "1.1.0-alpha05"

    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 offers additional features
    // to LocalStorage.
    implementation("androidx.appsearch:appsearch-platform-storage:$appsearch_version")
}

Conceptos de AppSearch

En el siguiente diagrama, se ilustran los conceptos de AppSearch y sus interacciones.

Diagrama del esquema de una aplicación cliente y sus interacciones con los siguientes conceptos de AppSearch: base de datos de AppSearch, esquema, tipos de esquemas, documentos, sesión y búsqueda. 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 muestra 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 consisten en 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 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.

Una clase de documento es una abstracción de un documento. Contiene campos con anotaciones que representan el contenido de un documento. De forma predeterminada, el nombre de la clase del documento establece el nombre del tipo de esquema.

Los documentos se indexan y se pueden buscar si se proporciona una consulta. Un documento se considera coincidente y se incluye en los resultados de la búsqueda si contiene los términos de la búsqueda 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 de tamaño de página y fragmento.

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 en todo el sistema. El índice de PlatformStorage se aloja en el servidor del sistema y el de PlayServicesStorage se aloja en el almacenamiento de los Servicios de Google Play. El acceso a los datos dentro de estos índices centrales se limita a los datos que tu aplicación contribuyó y a los que otra aplicación compartió de forma explícita contigo. Todas estas opciones de almacenamiento comparten la misma API y se pueden intercambiar según la versión de un dispositivo:

Kotlin

if (BuildCompat.isAtLeastS()) {
    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 (BuildCompat.isAtLeastS()) {
    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 realizar búsquedas en los datos de tu app. El uso compartido de datos de aplicaciones de solo lectura se otorga mediante 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 indexados se pueden mostrar en las plataformas de la IU del sistema. Las aplicaciones pueden inhabilitar que algunos o todos sus datos se muestren 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 lenguajes
Tamaño de objetos binarios reducido
Uso compartido de datos de aplicación a aplicación
Capacidad para mostrar datos en las plataformas de la IU del sistema
Se pueden indexar un número 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. Debido a que PlatformStorage une las APIs de Jetpack en el servicio del sistema 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 incurren en una latencia adicional del vinculador 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 es compatible con dispositivos que tengan los Servicios de Google Play.

Comienza a usar AppSearch

En el ejemplo de esta sección, se muestra cómo usar las APIs de AppSearch para integrarlas con una aplicación hipotética de toma de notas.

Cómo escribir una clase de documento

El primer paso para integrar AppSearch es escribir una clase de documento para describir los datos que se insertarán en la base de datos. Para marcar una clase como una clase de documento, usa la anotación @Document.Puedes usar instancias de la clase de documento para colocar documentos y recuperarlos de la base de datos.

En el siguiente código, se define una clase de documento de Note con un campo annotated @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;
  }
}

Cómo abrir 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()
);

Cómo establecer un esquema

Debes configurar un esquema antes de poder ingresar documentos y recuperarlos de la base de datos. El esquema de la base de datos consta de diferentes tipos de datos estructurados, denominados "tipos de esquemas". En el siguiente código, se configura 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);

Cómo colocar 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. El siguiente código compila un documento de tipo de esquema Note con el compilador de clase de documentos Note. Establece el espacio de nombres del documento user1 para representar un usuario arbitrario de esta muestra. 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 put.

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

Puedes buscar documentos indexados con las operaciones de búsqueda que se describen en esta sección. El siguiente código realiza consultas del término “fruta” en la base de datos para los 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 muestran una instancia de SearchResults, que brinda acceso a las páginas de objetos SearchResult. Cada SearchResult contiene su GenericDocument coincidente, el formato general de un documento al que se convierten todos los documentos. El siguiente código obtiene la primera página de los resultados de la búsqueda y vuelve a convertirlo 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);

Cómo quitar 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 aparezca en las consultas. El siguiente código 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);

Persistencia en el disco

Las actualizaciones de una base de datos deben persistirse periódicamente en el disco llamando a requestFlush(). El siguiente código 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 AppSearchSession cuando una aplicación ya no llame a ninguna operación de base de datos. El siguiente código cierra la sesión de AppSearch que se abrió anteriormente 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

Envía comentarios

Comparte tus comentarios e ideas con nosotros por medio de estos recursos:

Herramienta de seguimiento de errores

Informa los errores para que podamos solucionarlos.