App-Suche

AppSearch ist eine leistungsstarke On-Device-Suchlösung zur Verwaltung lokal gespeicherter, strukturierter Daten. Sie enthält APIs zum Indexieren von Daten und Abrufen von Daten über die Volltextsuche. Anwendungen können mit AppSearch benutzerdefinierte Suchfunktionen innerhalb der App anbieten, mit denen Nutzer auch offline nach Inhalten suchen können.

Diagramm zur Veranschaulichung der Indexierung und Suche in AppSearch

AppSearch bietet die folgenden Funktionen:

  • Eine schnelle Mobile-First-Speicherimplementierung mit geringer E/A-Nutzung
  • Hocheffiziente Indexierung und Abfrage großer Datensätze
  • Mehrsprachige Unterstützung, z. B. Englisch und Spanisch
  • Relevanzrangfolge und Nutzungsbewertung

Aufgrund der geringeren E/A-Nutzung bietet AppSearch im Vergleich zu SQLite eine geringere Latenz beim Indexieren und Suchen in großen Datasets. AppSearch vereinfacht typübergreifende Abfragen durch die Unterstützung einzelner Abfragen, während SQLite Ergebnisse aus mehreren Tabellen zusammenführt.

Um die Funktionen von AppSearch zu veranschaulichen, nehmen wir am Beispiel einer Musik-App, mit der die Lieblingssongs der Nutzer verwaltet werden und mit der Nutzer einfach danach suchen können. Nutzer hören Musik aus der ganzen Welt mit Songtiteln in verschiedenen Sprachen, für die AppSearch nativ die Indexierung und Abfrage unterstützt. Wenn der Nutzer anhand des Titels oder Künstlernamens nach einem Song sucht, übergibt die Anwendung die Anfrage einfach an AppSearch, um schnell und effizient übereinstimmende Titel abzurufen. Die Ergebnisse werden in der App angezeigt, sodass Nutzer schnell ihre Lieblingssongs abspielen können.

Einrichten

Damit AppSearch in Ihrer Anwendung verwendet werden kann, fügen Sie der Datei build.gradle Ihrer Anwendung die folgenden Abhängigkeiten hinzu:

Groovig

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

AppSearch-Konzepte

Das folgende Diagramm veranschaulicht AppSearch-Konzepte und ihre Interaktionen.

Schematische Darstellung einer Clientanwendung und ihrer Interaktionen mit den folgenden AppSearch-Konzepten: AppSearch-Datenbank, Schema, Schematypen, Dokumente, Sitzung und Suche. Abbildung 1: Diagramm der AppSearch-Konzepte: AppSearch-Datenbank, Schema, Schematypen, Dokumente, Sitzung und Suche.

Datenbank und Sitzung

Eine AppSearch-Datenbank ist eine Sammlung von Dokumenten, die dem Datenbankschema entspricht. Clientanwendungen erstellen eine Datenbank, indem sie den Anwendungskontext und einen Datenbanknamen angeben. Datenbanken können nur von der Anwendung geöffnet werden, von der sie erstellt wurden. Beim Öffnen einer Datenbank wird eine Sitzung zurückgegeben, um mit der Datenbank zu interagieren. Die Sitzung ist der Einstiegspunkt für den Aufruf der AppSearch APIs und bleibt geöffnet, bis sie von der Clientanwendung geschlossen wird.

Schema und Schematypen

Ein Schema stellt die Organisationsstruktur der Daten in einer AppSearch-Datenbank dar.

Das Schema besteht aus Schematypen, die eindeutige Datentypen darstellen. Schematypen bestehen aus Attributen, die einen Namen, einen Datentyp und eine Kardinalität enthalten. Nachdem ein Schematyp dem Datenbankschema hinzugefügt wurde, können Dokumente dieses Schematyps erstellt und der Datenbank hinzugefügt werden.

Dokumente

In AppSearch wird eine Dateneinheit als Dokument dargestellt. Jedes Dokument in einer AppSearch-Datenbank ist anhand seines Namespace und seiner ID eindeutig identifiziert. Namespaces werden verwendet, um Daten aus verschiedenen Quellen zu trennen, wenn nur eine Quelle abgefragt werden muss, z. B. Nutzerkonten.

Dokumente enthalten einen Erstellungszeitstempel, eine Gültigkeitsdauer (TTL) und einen Wert, der beim Abrufen für das Ranking verwendet werden kann. Einem Dokument wird auch ein Schematyp zugewiesen, der zusätzliche Dateneigenschaften beschreibt, die das Dokument haben muss.

Eine Dokumentklasse ist eine Abstraktion eines Dokuments. Sie enthält annotierte Felder, die den Inhalt eines Dokuments darstellen. Standardmäßig wird der Name des Schematyps durch den Namen der Dokumentklasse festgelegt.

Dokumente werden indexiert und können durch Angabe einer Abfrage durchsucht werden. Ein Dokument wird abgeglichen und in die Suchergebnisse aufgenommen, wenn es die Begriffe in der Abfrage enthält oder mit einer anderen Suchspezifikation übereinstimmt. Die Ergebnisse werden nach ihrem Punktzahl und ihrer Ranking-Strategie geordnet. Die Suchergebnisse werden durch Seiten dargestellt, die Sie nacheinander abrufen können.

AppSearch bietet Anpassungen für die Suche, z. B. Filter, Konfiguration der Seitengröße und Snippets.

Plattformspeicher vs. lokaler Speicher

AppSearch bietet zwei Speicherlösungen: LocalStorage und PlatformStorage. Mit LocalStorage verwaltet Ihre Anwendung einen anwendungsspezifischen Index, der sich in Ihrem Anwendungsdatenverzeichnis befindet. Mit PlatformStorage trägt Ihre Anwendung zu einem systemweiten zentralen Index bei. Der Datenzugriff im zentralen Index ist auf Daten beschränkt, die Ihre Anwendung beigesteuert hat, sowie auf Daten, die von einer anderen Anwendung explizit für Sie freigegeben wurden. LocalStorage und PlatformStorage nutzen dieselbe API und können je nach Geräteversion ausgetauscht werden:

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

Mit PlatformStorage kann Ihre Anwendung Daten sicher für andere Anwendungen freigeben, damit diese auch in den Daten Ihrer Anwendung suchen können. Die schreibgeschützte Freigabe von Anwendungsdaten wird über einen Zertifikat-Handshake gewährt, um sicherzustellen, dass die andere Anwendung die Berechtigung zum Lesen der Daten hat. Weitere Informationen zu dieser API finden Sie in der Dokumentation zu setSchemaType visibilityForPackage().

Darüber hinaus können indexierte Daten auf System-UI-Oberflächen angezeigt werden. Anwendungen können verhindern, dass einige oder alle ihrer Daten auf System-UI-Oberflächen angezeigt werden. Weitere Informationen zu dieser API finden Sie in der Dokumentation zu setSchemaTypeDisplayedBySystem().

Funktionen 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

Bei der Wahl zwischen LocalStorage und PlatformStorage müssen Sie zusätzliche Vor- und Nachteile berücksichtigen. Da PlatformStorage Jetpack APIs mit dem AppSearch-Systemdienst umschließt, ist die Auswirkung auf die APK-Größe im Vergleich zur Verwendung von LocalStorage minimal. Das bedeutet jedoch auch, dass AppSearch-Vorgänge beim Aufrufen des AppSearch-Systemdienstes eine zusätzliche Binder-Latenz verursachen. Mit PlatformStorage begrenzt AppSearch die Anzahl der Dokumente und die Größe der Dokumente, die eine Anwendung indexieren kann, um einen effizienten zentralen Index zu gewährleisten.

Erste Schritte mit AppSearch

Im Beispiel in diesem Abschnitt wird gezeigt, wie AppSearch APIs für die Integration in eine hypothetische Anwendung zur Notizenverwaltung verwendet werden.

Eine Dokumentklasse schreiben

Der erste Schritt für die Integration in AppSearch besteht darin, eine Dokumentklasse zu schreiben, um die Daten zu beschreiben, die in die Datenbank eingefügt werden sollen. Markieren Sie eine Klasse mithilfe der Annotation @Document als Dokumentklasse.Mit Instanzen der Dokumentklasse können Sie Dokumente einfügen und Dokumente aus der Datenbank abrufen.

Mit dem folgenden Code wird eine Notizdokumentklasse mit einem mit @Document.StringProperty annotierten Feld zum Indexieren des Texts eines Notizobjekts definiert.

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

Datenbank öffnen

Sie müssen eine Datenbank erstellen, bevor Sie mit Dokumenten arbeiten können. Mit dem folgenden Code wird eine neue Datenbank mit dem Namen notes_app erstellt und ein ListenableFuture für ein AppSearchSession abgerufen, das die Verbindung zur Datenbank darstellt und die APIs für Datenbankvorgänge bereitstellt.

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

Schema festlegen

Sie müssen ein Schema festlegen, bevor Sie Dokumente einfügen und Dokumente aus der Datenbank abrufen können. Das Datenbankschema besteht aus verschiedenen Typen strukturierter Daten, die als "Schematypen" bezeichnet werden. Mit dem folgenden Code wird das Schema festgelegt, indem die Dokumentklasse als Schematyp angegeben wird.

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

Ein Dokument in der Datenbank ablegen

Nachdem ein Schematyp hinzugefügt wurde, können Sie der Datenbank Dokumente dieses Typs hinzufügen. Mit dem folgenden Code wird ein Dokument mit dem Schematyp Note mit dem Dokumentklassen-Builder Note erstellt. Er legt den Dokument-Namespace user1 so fest, dass er einen beliebigen Nutzer dieses Beispiels darstellt. Das Dokument wird dann in die Datenbank eingefügt und ein Listener angehängt, um das Ergebnis des Put-Vorgangs zu verarbeiten.

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

Sie können mithilfe der in diesem Abschnitt beschriebenen Suchvorgänge nach Dokumenten suchen, die indexiert sind. Mit dem folgenden Code werden Abfragen für den Begriff „Obst“ über die Datenbank für Dokumente ausgeführt, die zum Namespace user1 gehören.

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

Suchergebnisse durchlaufen

Bei der Suche wird eine SearchResults-Instanz zurückgegeben, die Zugriff auf die Seiten von SearchResult-Objekten gewährt. Jede SearchResult enthält eine übereinstimmende GenericDocument, die allgemeine Form eines Dokuments, in das alle Dokumente konvertiert werden. Mit dem folgenden Code wird die erste Seite der Suchergebnisse abgerufen und das Ergebnis wieder in ein Note-Dokument konvertiert.

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

Dokumente entfernen

Wenn der Nutzer eine Notiz löscht, löscht die Anwendung das entsprechende Note-Dokument aus der Datenbank. Dadurch wird sichergestellt, dass der Hinweis nicht mehr in Abfragen angezeigt wird. Mit dem folgenden Code wird eine explizite Anfrage zum Entfernen des Note-Dokuments aus der Datenbank anhand der ID gestellt.

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

Auf Laufwerk beibehalten

Aktualisierungen einer Datenbank sollten durch Aufrufen von requestFlush() regelmäßig auf dem Laufwerk gespeichert werden. Mit dem folgenden Code wird requestFlush() mit einem Listener aufgerufen, um festzustellen, ob der Aufruf erfolgreich war.

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

Sitzung schließen

Ein AppSearchSession sollte geschlossen werden, wenn eine Anwendung keine Datenbankvorgänge mehr aufruft. Mit dem folgenden Code wird die zuvor geöffnete AppSearch-Sitzung geschlossen und alle Updates werden auf das Laufwerk übertragen.

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

Weitere Informationen

Weitere Informationen zu AppSearch finden Sie in den folgenden Ressourcen:

Produktproben

Feedback geben

Teilen Sie uns Ihr Feedback und Ihre Ideen über diese Ressourcen mit:

Problemverfolgung

Melden Sie Programmfehler, damit wir sie beheben können.