AppSearch

O AppSearch é uma solução de pesquisa no dispositivo de alto desempenho para gerenciamento local dados estruturados e armazenados. Ele contém APIs para indexar e recuperar dados usando a pesquisa de texto completo. Os aplicativos podem usar o AppSearch para oferecer anúncios personalizados no aplicativo recursos de pesquisa, permitindo que os usuários pesquisem conteúdo mesmo off-line.

Diagrama ilustrando a indexação e a pesquisa com o AppSearch

O AppSearch oferece os seguintes recursos:

  • Uma implementação de armazenamento rápida e mobile-first com baixo uso de E/S
  • Indexação e consultas altamente eficientes em grandes conjuntos de dados
  • Suporte a vários idiomas, como inglês e espanhol
  • Classificação de relevância e pontuação de uso

Devido ao menor uso de E/S, o AppSearch oferece menor latência para indexação e pesquisa em grandes conjuntos de dados em comparação com o SQLite. O AppSearch simplifica as consultas entre tipos oferecendo suporte a consultas únicas, enquanto o SQLite mescla resultados de várias tabelas.

Para ilustrar os recursos do AppSearch, vamos pegar o exemplo de uma aplicativo que gerencia as músicas favoritas dos usuários e permite que eles pesquisem facilmente para eles. Usuários ouvem músicas de todo o mundo com títulos em diferentes linguagens, que o AppSearch oferece suporte nativo a indexação e consultas. Quando o o usuário pesquisar uma música por título ou nome do artista, o aplicativo simplesmente passará a solicitação ao AppSearch para recuperar as músicas correspondentes de maneira rápida e eficiente. A o aplicativo exibe os resultados, permitindo que os usuários comecem a jogar suas músicas favoritas.

Configurar

Para usar o AppSearch no seu aplicativo, adicione as seguintes dependências ao seu arquivo build.gradle do aplicativo:

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha04"

    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-alpha04"

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

Conceitos do AppSearch

O diagrama a seguir ilustra os conceitos do AppSearch e as interações deles.

Diagrama
esboço de um aplicativo cliente e suas interações com os seguintes
Conceitos do AppSearch: banco de dados do AppSearch, esquema, tipos de esquema, documentos
sessão e pesquisa. Figura 1. Diagrama dos conceitos do AppSearch: banco de dados, esquema, esquemas, documentos, sessão e pesquisa.

Banco de dados e sessão

Um banco de dados do AppSearch é uma coleção de documentos que estão em conformidade com o banco de dados esquema. Os aplicativos clientes criam um banco de dados fornecendo o aplicativo contexto e um nome de banco de dados. Os bancos de dados só podem ser abertos pelo aplicativo que os criou. Quando um banco de dados é aberto, uma sessão é retornada para interagir com o banco de dados. A sessão é o ponto de entrada para chamar as APIs AppSearch e permanece aberto até ser fechado pelo aplicativo cliente.

Esquema e tipos de esquema

Um esquema representa a estrutura organizacional de dados em um AppSearch no seu banco de dados.

O esquema é composto por tipos que representam tipos únicos de dados. Os tipos de esquema consistem em propriedades que contêm um nome, tipo de dados e cardinalidade. Depois que um tipo de esquema é adicionado ao esquema do banco de dados, os documentos de esse tipo de esquema pode ser criado e adicionado ao banco de dados.

Documentos

No AppSearch, uma unidade de dados é representada como um documento. Cada documento em um O banco de dados AppSearch é identificado de forma exclusiva pelo namespace e ID. Namespaces são usados para separar dados de diferentes fontes quando apenas uma precisa que serão consultados, como contas de usuário.

Os documentos contêm um carimbo de data/hora da criação, um time to live (TTL) e uma pontuação que pode ser usada para classificação durante a recuperação. Um esquema também é atribuído a um documento que descreve as propriedades de dados adicionais que o documento deve ter.

Uma classe de documento é uma abstração de um documento. Contém campos com anotações que representam o conteúdo de um documento. Por padrão, o nome do documento define o nome do tipo de esquema.

Os documentos são indexados e podem ser pesquisados através de uma consulta. Um documento é correspondentes e incluídos nos resultados da pesquisa se contiverem os termos da consulta ou corresponde a outra especificação de pesquisa. Os resultados são ordenados com base em e a estratégia de classificação. Os resultados da pesquisa são representados por páginas que você pode recuperar sequencialmente.

O AppSearch oferece personalizações para pesquisa, como filtros, configuração do tamanho da página e snippeting.

Armazenamento da plataforma vs. armazenamento local

O AppSearch oferece duas soluções de armazenamento: LocalStorage e PlatformStorage. Com o LocalStorage, seu aplicativo gerencia um índice específico do aplicativo que reside o diretório de dados do aplicativo. Com o PlatformStorage, seu aplicativo contribui para um índice central de todo o sistema. Acesso a dados no índice central é restrito aos dados que seu aplicativo contribuiu e aos dados que foram compartilhados explicitamente com você por outro aplicativo. Tanto a LocalStorage quanto O PlatformStorage compartilham a mesma API e pode ser trocado com base no versão:

Kotlin

if (BuildCompat.isAtLeastS()) {
    appSearchSessionFuture.setFuture(
        PlatformStorage.createSearchSession(
            PlatformStorage.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 {
    mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
            new LocalStorage.SearchContext.Builder(mContext, DATABASE_NAME)
                    .build()));
}

Ao usar o PlatformStorage, seu aplicativo pode compartilhar dados de forma segura com outros para permitir que eles pesquisem os dados do app também. Somente leitura o compartilhamento de dados de aplicativo é concedido por um handshake de certificado para garantir que o outro aplicativo tem permissão para ler os dados. Leia mais sobre esta API na documentação de setSchemaTypeVisibilityForPackage().

Além disso, os dados indexados podem ser mostrados nas plataformas da interface do sistema. Os aplicativos podem desativar a exibição de alguns ou de todos os dados no sistema Plataformas de interface. Leia mais sobre essa API na documentação de setSchemaTypeDisplayedBySystem().

Recursos LocalStorage (compatible with Android 4.0+) PlatformStorage (compatible with Android 12+)
Efficient full-text search
Multi-language support
Reduced binary size
Application-to-application data sharing
Capability to display data on System UI surfaces
Unlimited document size and count can be indexed
Faster operations without additional binder latency

Há outras vantagens e desvantagens a serem consideradas ao escolher entre LocalStorage e PlatformStorage. Como o PlatformStorage une as APIs do Jetpack ao AppSearch, o impacto no tamanho do APK é mínimo em comparação com o uso Armazenamento local. No entanto, isso também significa que as operações do AppSearch geram latência do binder ao chamar o serviço do sistema AppSearch. Com o PlatformStorage, O AppSearch limita o número de documentos e o tamanho de documentos que um aplicativo possa indexar para garantir um índice central eficiente.

Comece a usar o AppSearch

O exemplo nesta seção mostra como usar as APIs AppSearch para fazer a integração com um aplicativo hipotético de anotações.

Escrever uma classe de documentos

A primeira etapa para fazer a integração com o AppSearch é criar uma classe de documento para descrever os dados a serem inseridos no banco de dados. Marcar uma turma como uma classe de documento usando o @Document anotação.Você pode usar instâncias da classe document para colocar documentos em e e recuperar documentos do banco de dados.

O código a seguir define uma classe de documento Note com uma com a anotação @Document.StringProperty para indexar o texto de um 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;
  }
}

Abrir um banco de dados

É necessário criar um banco de dados antes de trabalhar com documentos. O código a seguir cria um novo banco de dados com o nome notes_app e recebe um ListenableFuture para um AppSearchSession, que representa a conexão com o banco de dados e fornece as APIs para operações de banco de dados.

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

Definir um esquema

É necessário definir um esquema antes de colocar documentos e recuperá-los documentos do banco de dados. O esquema do banco de dados consiste em tipos diferentes de dados estruturados, chamados de "tipos de esquema". O código a seguir define fornecendo a classe Document como um 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);

Colocar um documento no banco de dados

Depois que um tipo de esquema é adicionado, é possível adicionar documentos desse tipo ao banco de dados. O código a seguir cria um documento do tipo de esquema Note usando a Note Builder de classes de documentos. Ele define o namespace do documento user1 para representar um usuário arbitrário desta amostra. Em seguida, o documento é inserido no banco de dados e um listener é anexado para processar o resultado da operação 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);

Você pode pesquisar documentos indexados usando as operações de pesquisa abordadas em nesta seção. O código a seguir executa consultas pelo termo "fruit" sobre banco de dados para documentos que pertencem ao namespace 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);

Iterar com SearchResults

As pesquisas retornam SearchResults. que dá acesso às páginas dos objetos SearchResult. Cada SearchResult contém o GenericDocument correspondente, a forma geral de uma no qual todos os documentos são convertidos. O código a seguir recebe a primeira página de resultados da pesquisa e converte o resultado de volta em um 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);

Remover um documento

Quando o usuário exclui uma observação, o aplicativo exclui a Note correspondente documento do banco de dados. Isso garante que a nota não será mais exibida consultas. O código a seguir faz uma solicitação explícita para remover o Note. documento do banco de dados 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);

Manter no disco

As atualizações em um banco de dados devem ser mantidas periodicamente no disco chamando requestFlush() A O código a seguir chama requestFlush() com um listener para determinar se a chamada deu certo.

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

Encerrar uma sessão

Um AppSearchSession precisará ser fechado quando um aplicativo não chamar mais nenhum banco de dados. as operações. O código a seguir fecha a sessão do AppSearch que foi aberta e mantém todas as atualizações no 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);

Outros recursos

Para saber mais sobre o AppSearch, consulte os recursos a seguir:

Amostras

Enviar feedback

Envie comentários e ideias usando os recursos abaixo:

Issue Tracker (link em inglês)

Informe os bugs para que possamos corrigi-los.