AppSearch

AppSearch est une solution de recherche hautes performances sur l'appareil qui permet de gérer localement stockées et structurées. Il contient des API d'indexation et de récupération de données à l'aide de la recherche en texte intégral. Les applications peuvent utiliser AppSearch pour proposer des créations personnalisées intégrées aux applications de recherche, permettant aux utilisateurs de rechercher du contenu même hors connexion.

Diagramme illustrant l'indexation et la recherche dans AppSearch

AppSearch offre les fonctionnalités suivantes:

  • Une implémentation rapide du stockage mobile avec une faible utilisation des E/S
  • Indexation et interrogation efficaces sur de grands ensembles de données
  • Compatibilité avec plusieurs langues (anglais et espagnol, par exemple)
  • Classement par pertinence et évaluation de l'utilisation

En raison de l'utilisation réduite des E/S, AppSearch offre une latence plus faible pour l'indexation et la recherche. sur de grands ensembles de données par rapport à SQLite. AppSearch simplifie les requêtes multitypes en acceptant des requêtes uniques, tandis que SQLite fusionne les résultats de plusieurs tables.

Pour illustrer les fonctionnalités d'AppSearch, prenons l'exemple d'une bibliothèque musicale qui gère les chansons préférées des utilisateurs et leur permet de rechercher facilement pour eux. Les utilisateurs apprécient la musique du monde entier, avec différents titres pour lesquelles AppSearch est compatible nativement avec l'indexation et l'interrogation. Lorsque recherche une chanson par titre ou par nom d'artiste, l'application transmet la requête à AppSearch pour récupérer rapidement et efficacement les titres correspondants. La affiche les résultats pour permettre aux utilisateurs de commencer à jouer leurs chansons préférées.

Configuration

Pour utiliser AppSearch dans votre application, ajoutez les dépendances suivantes à votre le fichier build.gradle de l'application:

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha06"

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

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

Concepts AppSearch

Le schéma suivant illustre les concepts d'AppSearch et leurs interactions.

Diagramme
d'une application cliente et de ses interactions avec
Concepts AppSearch: base de données, schéma, types de schémas, documents,
les sessions et la recherche. Figure 1 : Schéma des concepts d'AppSearch: base de données, schéma et les types de schémas, documents, session et recherche.

Base de données et session

Une base de données AppSearch est une collection de documents conformes à la base de données. du schéma. Les applications clientes créent une base de données en fournissant leur application le contexte et un nom de base de données. Les bases de données ne peuvent être ouvertes que par l'application qui les a créés. Lorsqu'une base de données est ouverte, une session est renvoyée pour interagir. avec la base de données. La session est le point d'entrée pour appeler les API AppSearch et reste ouvert jusqu'à sa fermeture par l'application cliente.

Schéma et types de schémas

Un schéma représente la structure organisationnelle des données dans une application AppSearch. base de données.

Le schéma est composé de types de schémas qui représentent des types de données uniques. Les types de schémas se composent de propriétés qui contiennent un nom, un type de données et la cardinalité. Une fois qu'un type de schéma est ajouté au schéma de base de données, les documents de ce type de schéma peut être créé et ajouté à la base de données.

Documents

Dans AppSearch, une unité de données est représentée sous la forme d'un document. Chaque document d'une classe La base de données AppSearch est identifiée de manière unique par son espace de noms et son ID. Espaces de noms sont utilisés pour séparer les données de différentes sources lorsqu'une seule source a besoin telles que les comptes d'utilisateurs.

Les documents contiennent un code temporel de création, une valeur TTL (Time To Live) et un score qui peuvent être utilisées pour le classement lors de la récupération. Un schéma est également attribué à un document qui décrit les propriétés de données supplémentaires que le document doit comporter.

Une classe Document est une abstraction d'un document. Elle contient des champs annotés qui représentent le contenu d'un document. Par défaut, le nom du document définit le nom du type de schéma.

Les documents sont indexés et peuvent faire l'objet d'une recherche en soumettant une requête. Un document est mis en correspondance et inclus dans les résultats de recherche s'il contient les termes de la requête ou correspond à une autre spécification de recherche. Les résultats sont classés en fonction de votre score et votre stratégie de classement. Les résultats de recherche sont représentés par des pages de manière séquentielle.

AppSearch permet de personnaliser pour la recherche, comme les filtres, la configuration de la taille de page et les extraits.

Stockage sur la plate-forme ou stockage local

AppSearch propose deux solutions de stockage: LocalStorage et PlatformStorage. Avec LocalStorage, votre application gère un index spécifique à l'application qui réside dans le répertoire de données de votre application. Avec PlatformStorage, votre application contribue à créer un index central à l'échelle du système. Accès aux données dans l'index central est limitée aux données fournies par votre application et à celles qui ont été explicitement partagés avec vous par une autre application. LocalStorage et PlatformStorage partage la même API et est interchangeables version:

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

PlatformStorage permet à votre application de partager des données en toute sécurité avec d'autres applications afin de leur permettre d'effectuer des recherches dans les données de votre application. Lecture seule le partage des données de l'application est accordé par un handshake de certificat pour garantir que l'autre application est autorisée à lire les données. En savoir plus sur cette API dans la documentation sur setSchemaTypeVisibilityForPackage().

De plus, les données indexées peuvent être affichées sur les surfaces de l'UI du système. Les applications peuvent désactiver l'affichage de tout ou partie de leurs données sur le système surfaces d'UI. Pour en savoir plus sur cette API, consultez la documentation sur setSchemaTypeDisplayedBySystem().

Fonctionnalités 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

Il existe d'autres compromis à prendre en compte lorsque vous choisissez entre le stockage local et PlatformStorage. Comme PlatformStorage encapsule les API Jetpack sur le service système AppSearch, l'impact sur la taille de l'APK est minime Stockage local. Toutefois, cela signifie également que les opérations AppSearch entraînent la latence de liaison lors de l'appel du service système AppSearch. Avec PlatformStorage, AppSearch limite le nombre et la taille des documents qu'une application peut indexer pour garantir un index central efficace.

Premiers pas avec AppSearch

L'exemple de cette section montre comment utiliser les API AppSearch pour intégrer avec une application de prise de notes fictive.

Écrire une classe Document

Pour intégrer AppSearch, la première étape consiste à écrire une classe Document dans décrire les données à insérer dans la base de données. Marquer une classe comme classe de document à l'aide de la méthode @Document Vous pouvez utiliser des instances de la classe Document pour placer des documents dans et récupérer des documents de la base de données.

Le code suivant définit une classe Note Document avec un élément @Document.StringProperty annoté permettant d'indexer le texte d'un objet 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;
  }
}

Ouvrir une base de données

Vous devez créer une base de données avant de travailler sur des documents. Le code suivant crée une base de données nommée notes_app et obtient un ListenableFuture. pour un AppSearchSession, qui représente la connexion à la base de données et fournit les API pour les opérations de base de données.

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

Définir un schéma

Vous devez définir un schéma avant de pouvoir placer des documents dans et récupérer de la base de données. Le schéma de base de données comprend différents types de données structurées, ou "types de schémas". Le code suivant définit le en fournissant la classe Document en tant que type de schéma.

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

Placer un document dans la base de données

Une fois qu'un type de schéma est ajouté, vous pouvez ajouter des documents de ce type à la base de données. Le code suivant crée un document de type de schéma Note à l'aide de l'Note le générateur de classes de document. Elle définit l'espace de noms du document user1 pour représenter un utilisateur arbitraire de cet échantillon. Le document est ensuite inséré dans la base de données. et un écouteur est associé pour traiter le résultat de l'opération 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);

Vous pouvez rechercher des documents indexés à l'aide des opérations de recherche abordées dans cette section. Le code suivant exécute des requêtes sur le terme "fruit" au-dessus du base de données pour les documents appartenant à l'espace de noms 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);

Itérer dans les résultats de recherche

Les recherches renvoient un SearchResults qui donne accès aux pages d'objets SearchResult. Chaque SearchResult contient son GenericDocument correspondant, qui se présente généralement sous la forme document vers lequel tous les documents sont convertis. Le code suivant permet d'obtenir la première page des résultats de recherche et reconvertit le résultat dans un document 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);

Supprimer un document

Lorsque l'utilisateur supprime une note, l'application supprime le Note correspondant. de la base de données. Ainsi, la note n'apparaîtra plus dans requêtes. Le code suivant envoie une requête explicite pour supprimer Note. document à partir de la base de données par 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);

Conserver sur le disque

Les mises à jour d'une base de données doivent être conservées régulièrement sur le disque en appelant requestFlush() La le code suivant appelle requestFlush() avec un écouteur pour déterminer si l'appel a réussi.

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

Fermer une session

Un AppSearchSession doit être fermée lorsqu'une application n'appelle plus aucune base de données opérations. Le code suivant ferme la session AppSearch ouverte et conserve toutes les mises à jour sur le disque.

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

Ressources supplémentaires

Pour en savoir plus sur AppSearch, consultez les ressources supplémentaires suivantes:

Exemples

  • Exemple Android AppSearch (Kotlin) Une application de prise de notes qui utilise AppSearch pour indexer les notes d'un utilisateur et lui permettre de faire des recherches dans leurs notes.

Envoyer des commentaires

Faites-nous part de vos commentaires et de vos idées via les ressources suivantes :

Issue Tracker

Signalez les bugs pour que nous puissions les corriger.