RicercaApp

AppSearch è una soluzione di ricerca on-device ad alte prestazioni per la gestione dei dati strutturati archiviati localmente. Contiene API per l'indicizzazione dei dati e il recupero dei dati mediante la ricerca a testo intero. Le applicazioni possono usare AppSearch per offrire funzionalità di ricerca in-app personalizzate, consentendo agli utenti di cercare contenuti anche in modalità offline.

Diagramma che illustra l'indicizzazione e la ricerca in AppSearch

AppSearch offre le seguenti funzionalità:

  • Un'implementazione rapida dell'archiviazione mobile-first con un basso utilizzo di I/O
  • Indicizzazione e query ad alta efficienza su grandi set di dati
  • Supporto di più lingue, ad esempio inglese e spagnolo
  • Ranking della pertinenza e punteggio di utilizzo

A causa del minor utilizzo di I/O, AppSearch offre una latenza inferiore per l'indicizzazione e la ricerca su set di dati di grandi dimensioni rispetto a SQLite. AppSearch semplifica le query in più tipi supportando query singole, mentre SQLite unisce i risultati di più tabelle.

Per illustrare le funzionalità di AppSearch, prendiamo come esempio un'applicazione musicale che gestisce i brani preferiti degli utenti e consente agli utenti di cercarli facilmente. Gli utenti ascoltano musica di tutto il mondo con titoli di brani in diverse lingue, per le quali AppSearch supporta l'indicizzazione e le query in modo nativo. Quando l'utente cerca un brano per titolo o nome dell'artista, l'applicazione passa semplicemente la richiesta ad AppSearch per recuperare in modo rapido ed efficiente i brani corrispondenti. L'applicazione mostra i risultati, consentendo agli utenti di iniziare rapidamente a riprodurre i loro brani preferiti.

Configurazione

Per utilizzare AppSearch nella tua applicazione, aggiungi le seguenti dipendenze al file build.gradle dell'applicazione:

Trendy

dependencies {
    def appsearch_version = "1.1.0-alpha03"

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

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

Concetti di AppSearch

Il seguente diagramma illustra i concetti di AppSearch e le loro interazioni.

Diagramma
di un'applicazione client e delle sue interazioni con i seguenti
concetti di AppSearch: database AppSearch, schema, tipi di schema, documenti,
sessione e ricerca. Figura 1. Diagramma dei concetti di AppSearch: database, schema, tipi di schema, documenti, sessioni e ricerca di AppSearch.

Database e sessione

Un database AppSearch è una raccolta di documenti conforme allo schema di database. Le applicazioni client creano un database fornendo il proprio contesto di applicazione e un nome del database. I database possono essere aperti solo dall'applicazione che li ha creati. Quando un database viene aperto, viene restituita una sessione per interagire con il database. La sessione è il punto di ingresso per le chiamate alle API AppSearch e rimane aperta fino a quando non viene chiusa dall'applicazione client.

Tipi di schemi

Uno schema rappresenta la struttura organizzativa dei dati all'interno di un database AppSearch.

Lo schema è composto da tipi che rappresentano tipi unici di dati. I tipi di schema sono costituiti da proprietà che contengono un nome, un tipo di dati e una cardinalità. Dopo aver aggiunto un tipo di schema allo schema del database, è possibile creare documenti di quel tipo e aggiungerli al database.

Documenti

In AppSearch, un'unità di dati è rappresentata come un documento. Ogni documento in un database AppSearch viene identificato in modo univoco dal suo spazio dei nomi e dall'ID. Gli spazi dei nomi vengono utilizzati per separare i dati da origini diverse quando è necessario eseguire query su una sola origine, ad esempio gli account utente.

I documenti contengono un timestamp di creazione, una durata (TTL) e un punteggio che può essere utilizzato per il ranking durante il recupero. A un documento viene anche assegnato un tipo di schema che descrive ulteriori proprietà dei dati che il documento deve avere.

Una classe Documento è un'astrazione di un documento. Contiene campi annotati che rappresentano i contenuti di un documento. Per impostazione predefinita, il nome della classe documento imposta il nome del tipo di schema.

I documenti vengono indicizzati ed è possibile eseguire ricerche fornendo una query. Un documento trova una corrispondenza e viene incluso nei risultati di ricerca se contiene i termini contenuti nella query o corrisponde a un'altra specifica di ricerca. I risultati sono ordinati in base al punteggio e alla strategia di ranking. I risultati di ricerca sono rappresentati da pagine che è possibile recuperare in sequenza.

AppSearch offre personalizzazioni per la ricerca, ad esempio filtri, configurazione delle dimensioni delle pagine e snippet.

Spazio di archiviazione della piattaforma e archiviazione locale

AppSearch offre due soluzioni di archiviazione: LocalStorage e PlatformStorage. Con LocalStorage, l'applicazione gestisce un indice specifico dell'app che si trova nella directory dei dati dell'applicazione. Con PlatformStorage, la tua applicazione contribuisce a un indice centrale a livello di sistema. L'accesso ai dati all'interno dell'indice centrale è limitato ai dati forniti dall'applicazione e ai dati esplicitamente condivisi con te da un'altra applicazione. LocalStorage e PlatformStorage condividono la stessa API e possono essere scambiati in base alla versione di un dispositivo:

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

Grazie a PlatformStorage, l'applicazione può condividere in modo sicuro i dati con altre applicazioni per consentire anche a loro di eseguire ricerche nei dati dell'app. La condivisione dei dati delle applicazioni di sola lettura è concessa tramite un handshake del certificato per garantire che l'altra applicazione abbia l'autorizzazione a leggere i dati. Scopri di più su questa API nella documentazione relativa a setSchemaTypeVisibilityForPackage().

Inoltre, i dati indicizzati possono essere visualizzati sulle piattaforme UI di sistema. Le applicazioni possono disattivare la visualizzazione di alcuni o tutti i dati sulle piattaforme dell'interfaccia utente di sistema. Scopri di più su questa API nella documentazione relativa a setSchemaTypeDisplayedBySystem().

Funzionalità 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

Esistono ulteriori compromessi da considerare nella scelta tra LocalStorage e PlatformStorage. Poiché PlatformStorage esegue l'aggregazione delle API Jetpack nel servizio di sistema AppSearch, l'impatto sulle dimensioni degli APK è minimo rispetto all'utilizzo di LocalStorage. Tuttavia, ciò significa anche che le operazioni AppSearch comportano un'ulteriore latenza di Binder quando si chiamano il servizio di sistema AppSearch. Con PlatformStorage, AppSearch limita il numero di documenti e le dimensioni dei documenti che un'applicazione può indicizzare per garantire un indice centrale efficiente.

Inizia a utilizzare AppSearch

L'esempio in questa sezione mostra come utilizzare le API AppSearch per l'integrazione con un'ipotetica applicazione di gestione delle note.

Scrivere un corso di documenti

Il primo passaggio per l'integrazione con AppSearch è scrivere una classe di documenti per descrivere i dati da inserire nel database. Contrassegna una classe come classe di documenti utilizzando l'annotazione @Document.Puoi utilizzare istanze della classe di documenti per inserire e recuperare documenti dal database.

Il codice seguente definisce una classe di documenti Note con un campo annotato @Document.StringProperty per l'indicizzazione del testo di un oggetto 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;
  }
}

Apri un database

Devi creare un database prima di lavorare con i documenti. Il codice seguente crea un nuovo database con il nome notes_app e ottiene un ListenableFuture per AppSearchSession, che rappresenta la connessione al database e fornisce le API per le operazioni sul database.

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

Imposta uno schema

Devi impostare uno schema prima di poter inserire documenti e recuperare documenti dal database. Lo schema del database è costituito da diversi tipi di dati strutturati, detti "tipi di schema". Il codice seguente imposta lo schema fornendo la classe document come tipo di schema.

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

Inserisci un documento nel database

Dopo aver aggiunto un tipo di schema, puoi aggiungere al database i relativi documenti. Il codice seguente crea un documento di tipo di schema Note utilizzando lo strumento per la creazione delle classi di documenti Note. Imposta lo spazio dei nomi del documento user1 per rappresentare un utente arbitrario di questo esempio. Il documento viene quindi inserito nel database e viene collegato un listener per elaborare il risultato dell'operazione 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);

Puoi cercare documenti indicizzati utilizzando le operazioni di ricerca trattate in questa sezione. Il codice seguente esegue query per il termine "frutto" sul database per i documenti che appartengono allo spazio dei nomi 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);

Ripetizione tramite i risultati di ricerca

Le ricerche restituiscono un'istanza SearchResults, che consente di accedere alle pagine degli oggetti SearchResult. Ogni SearchResult contiene il relativo GenericDocument, la forma generale di documento in cui vengono convertiti tutti i documenti. Il codice seguente recupera la prima pagina dei risultati di ricerca e li converte nuovamente in 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);

Rimuovere un documento

Quando l'utente elimina una nota, l'applicazione elimina il documento Note corrispondente dal database. In questo modo, la nota non verrà più visualizzata nelle query. Il codice seguente effettua una richiesta esplicita di rimuovere il documento Note dal database in base all'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);

Mantieni su disco

Gli aggiornamenti di un database devono essere periodicamente resi persistenti su disco chiamando requestFlush(). Il codice seguente chiama requestFlush() con un listener per determinare se la chiamata è riuscita.

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

Chiudere una sessione

È necessario chiudere AppSearchSession quando un'applicazione non chiama più operazioni del database. Il codice seguente chiude la sessione di AppSearch aperta in precedenza e riporta tutti gli aggiornamenti sul 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);

Risorse aggiuntive

Per scoprire di più su AppSearch, consulta le seguenti risorse aggiuntive:

Samples

  • Android AppSearch Sample (Kotlin), un'app per prendere appunti che utilizza AppSearch per indicizzare le note di un utente e consente agli utenti di eseguire ricerche nelle note.

Fornisci feedback

Condividi il tuo feedback e le tue idee con noi attraverso queste risorse:

Monitoraggio dei problemi

Segnala i bug per consentirci di correggerli.