Wyszukiwanie aplikacji

AppSearch to wydajne rozwiązanie do wyszukiwania na urządzeniu służące do zarządzania lokalnie przechowywanymi uporządkowanymi danymi. Zawiera interfejsy API do indeksowania danych i pobierania danych za pomocą wyszukiwania pełnotekstowego. Aplikacje mogą korzystać z AppSearch, aby oferować własne funkcje wyszukiwania w aplikacji, dzięki czemu użytkownicy mogą wyszukiwać treści nawet bez połączenia z internetem.

Diagram pokazujący indeksowanie i wyszukiwanie w AppSearch

AppSearch zapewnia następujące funkcje:

  • Szybka implementacja pamięci masowej przeznaczona na urządzenia mobilne o niskim wykorzystaniu wejścia/wyjścia
  • Efektywne indeksowanie dużych zbiorów danych i wykonywanie na nich zapytań
  • obsługa wielu języków, takich jak angielski i hiszpański.
  • Ranking trafności i ocena wykorzystania

Ze względu na mniejsze wykorzystanie operacji wejścia-wyjścia usługa AppSearch oferuje krótszy czas oczekiwania przy indeksowaniu i wyszukiwaniu dużych zbiorów danych w porównaniu z SQLite. AppSearch upraszcza zapytania krzyżowe, obsługując pojedyncze zapytania, natomiast SQLite scala wyniki z wielu tabel.

Aby zilustrować funkcje AppSearch, przyjrzyjmy się aplikacji muzycznej, która zarządza ulubionymi utworami użytkowników i umożliwia ich łatwe wyszukiwanie. Użytkownicy słuchają muzyki z całego świata i mają tytuły utworów w różnych językach. AppSearch natywnie obsługuje indeksowanie i wyszukiwanie haseł. Gdy użytkownik wyszukuje utwór według tytułu lub nazwy wykonawcy, aplikacja po prostu przekazuje żądanie do AppSearch, aby szybko i sprawnie pobrać pasujące utwory. Aplikacja wyświetla wyniki, umożliwiając użytkownikom szybkie rozpoczęcie odtwarzania ulubionych utworów.

Skonfiguruj

Aby użyć AppSearch w swojej aplikacji, dodaj te zależności do jej pliku build.gradle:

Odlotowy

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

Pojęcia związane z AppSearch

Poniższy diagram przedstawia pojęcia związane z AppSearch i ich interakcje.

Diagram aplikacji klienckiej i jej interakcji z następującymi pojęciami związanymi z AppSearch: baza danych AppSearch, schemat, typy schematów, dokumenty, sesja i wyszukiwanie. Rysunek 1. Diagram pojęć związanych z AppSearch: baza danych, schemat, typy schematów, dokumenty, sesja i wyszukiwanie.

Baza danych i sesja

Baza danych AppSearch to zbiór dokumentów zgodnych ze schematem bazy danych. Aplikacje klienckie tworzą bazę danych, podając kontekst aplikacji i nazwę bazy danych. Bazy danych można otwierać tylko przez aplikację, która je utworzyła. Po otwarciu bazy danych zwracana jest sesja umożliwiająca interakcję z bazą danych. Sesja jest punktem wejścia do wywoływania interfejsów AppSearch API i pozostaje otwarta, dopóki aplikacja kliencka nie zamknie jej.

Typy schematów i schematów

Schemat reprezentuje strukturę organizacyjną danych w bazie danych AppSearch.

Schemat składa się z typów schematów, które reprezentują unikalne typy danych. Typy schematów składają się z właściwości zawierających nazwę, typ danych i moc zbioru. Po dodaniu typu schematu do schematu bazy danych można tworzyć dokumenty tego typu i dodawać je do bazy danych.

Dokumenty

W AppSearch jednostka danych jest przedstawiana jako dokument. Każdy dokument w bazie danych AppSearch jest jednoznacznie identyfikowany przez przestrzeń nazw i identyfikator. Przestrzenie nazw służą do oddzielania danych z różnych źródeł, gdy konieczne jest przeszukiwanie tylko jednego źródła, na przykład kont użytkowników.

Dokumenty zawierają sygnaturę czasową utworzenia, czas życia danych (TTL) i wynik, który może służyć do określania pozycji dokumentów w rankingu podczas pobierania. Do dokumentu jest też przypisany typ schematu, który opisuje dodatkowe właściwości danych, jakie musi on mieć.

Klasa dokumentu to abstrakcja dokumentu. Zawiera pola z adnotacjami, które odpowiadają zawartości dokumentu. Domyślnie nazwa klasy dokumentu określa nazwę typu schematu.

Dokumenty są indeksowane i można je wyszukiwać, podając zapytanie. Dokument jest uwzględniany i uwzględniany w wynikach wyszukiwania, jeśli zawiera wyrażenia w zapytaniu lub spełnia inne kryteria wyszukiwania. Wyniki są uporządkowane na podstawie wyniku i strategii ustalania pozycji. Wyniki wyszukiwania są reprezentowane przez strony, które można pobierać sekwencyjnie.

AppSearch udostępnia dostosowania wyszukiwania, takie jak filtry, konfiguracja rozmiaru strony i fragmenty kodu.

Miejsce na dane na platformie a pamięć lokalna

AppSearch oferuje dwa rozwiązania do przechowywania danych: LocalStorage i PlatformStorage. Dzięki LocalStorage aplikacja zarządza indeksem dotyczącym aplikacji, który znajduje się w katalogu danych aplikacji. Dzięki PlatformStorage Twoja aplikacja wpływa na centralny indeks całego systemu. Dostęp do danych w indeksie centralnym jest ograniczony do danych udostępnionych przez Twoją aplikację oraz danych udostępnionych Ci bezpośrednio przez inną aplikację. Lokalna pamięć masowa i PlatformStorage korzystają z tego samego interfejsu API i mogą być wymieniane w zależności od wersji urządzenia:

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

Dzięki PlatformStorage Twoja aplikacja może bezpiecznie udostępniać dane innym aplikacjom, aby umożliwić im przeszukiwanie danych aplikacji. Udostępnianie danych aplikacji tylko do odczytu odbywa się przez uzgadnianie połączenia certyfikatu, aby zapewnić, że inna aplikacja ma uprawnienia do odczytu danych. Więcej informacji o tym interfejsie API znajdziesz w dokumentacji funkcji setSchemaTypeAvailabilityForPackage().

Dodatkowo zindeksowane dane mogą być wyświetlane na platformach UI systemu. Aplikacje mogą zrezygnować z wyświetlania niektórych lub wszystkich swoich danych na platformach UI systemu. Więcej informacji o tym interfejsie API znajdziesz w dokumentacji funkcji setSchemaTypeDisplayedBySystem().

Funkcje 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

Wybór między usługami LocalStorage i PlatformStorage wymaga rozważenia dodatkowych kompromisów. Platform Storage pakuje interfejsy API Jetpack z usługą systemową AppSearch, więc wpływ na rozmiar pliku APK jest minimalny w porównaniu z użyciem LocalStorage. Oznacza to jednak również, że operacje AppSearch ponoszą dodatkowe opóźnienie podczas wywoływania usługi systemowej AppSearch. Dzięki PlatformStorage AppSearch ogranicza liczbę dokumentów i rozmiar dokumentów, które aplikacja może indeksować, aby zapewnić wydajny indeks centralny.

Wprowadzenie do AppSearch

Przykład w tej sekcji pokazuje, jak za pomocą interfejsów API AppSearch przeprowadzić integrację z hipotetyczną aplikacją do przechowywania notatek.

Napisz klasę dokumentu

Pierwszym krokiem do przeprowadzenia integracji z AppSearch jest napisanie klasy dokumentu do opisania danych, które mają zostać wstawione do bazy danych. Oznacz klasę jako klasę dokumentu za pomocą adnotacji @Document.Za pomocą instancji klasy dokumentu możesz umieszczać dokumenty i pobierać dokumenty z bazy danych.

Poniższy kod określa klasę dokumentu Notatnika z polem z adnotacją @Document.StringProperty do indeksowania tekstu obiektu notatki.

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

Otwieranie bazy danych

Przed rozpoczęciem pracy z dokumentami musisz utworzyć bazę danych. Poniższy kod tworzy nową bazę danych o nazwie notes_app i pobiera ListenableFuture dla elementu AppSearchSession, który reprezentuje połączenie z bazą danych i udostępnia interfejsy API do operacji na bazie danych.

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

Ustaw schemat

Aby umieszczać dokumenty w bazie danych i pobierać je z bazy danych, musisz skonfigurować schemat. Schemat bazy danych składa się z różnych typów uporządkowanych danych nazywanych „typami schematu”. Poniższy kod określa schemat, podając klasę dokumentu jako typ schematu.

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

Umieść dokument w bazie danych

Po dodaniu schematu możesz dodać do bazy danych dokumenty tego typu. Poniższy kod umożliwia utworzenie dokumentu typu schematu Note za pomocą konstruktora klas dokumentu Note. Ustawia ona przestrzeń nazw dokumentu user1, która reprezentuje dowolnego użytkownika tego przykładu. Następnie dokument jest wstawiany do bazy danych i dołączany do odbiornika, który przetwarza wynik operacji „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);

Dokumenty zindeksowane możesz wyszukiwać, korzystając z operacji wyszukiwania opisanych w tej sekcji. Poniższy kod wykonuje zapytania o termin „owoc” nad bazą danych dokumentów należących do przestrzeni nazw 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);

Powtórzenie wyników wyszukiwania

Wyszukiwania zwracają wystąpienie SearchResults, które zapewnia dostęp do stron obiektów SearchResult. Każdy SearchResult zawiera dopasowane do siebie GenericDocument, czyli ogólną formę dokumentu, na który są konwertowane wszystkie dokumenty. Ten kod pobiera pierwszą stronę wyników wyszukiwania i konwertuje wynik z powrotem na dokument 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);

Usuwanie dokumentu

Gdy użytkownik usunie notatkę, aplikacja usunie z bazy danych odpowiedni dokument Note. Dzięki temu notatka nie będzie się już pojawiać w zapytaniach. Ten kod wysyła jawne żądanie usunięcia dokumentu Note z bazy danych za pomocą jego identyfikatora.

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

Zachowuj na dysku

Aktualizacje bazy danych powinny być okresowo zapisywane na dysku przez wywołanie requestFlush(). Ten kod wywołuje funkcję requestFlush() za pomocą detektora, aby określić, czy wywołanie się powiodło.

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

Zamykanie sesji

Obiekt AppSearchSession powinien być zamknięty, gdy aplikacja nie będzie już wywoływać żadnych operacji bazy danych. Poniższy kod zamyka wcześniej otwartą sesję AppSearch i utrzymuje wszystkie aktualizacje dysku.

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

Dodatkowe materiały

Więcej informacji o AppSearch znajdziesz w tych dodatkowych materiałach:

Próbki

Prześlij opinię

Podziel się z nami swoją opinią i pomysłami, korzystając z tych zasobów:

Śledzenie problemów

Zgłoś błędy, abyśmy mogli je naprawić.