AppSearch

AppSearch adalah solusi penelusuran di perangkat berperforma tinggi untuk mengelola data terstruktur yang disimpan secara lokal. Library ini berisi API untuk mengindeks data dan mengambil data menggunakan penelusuran teks lengkap. Aplikasi dapat menggunakan AppSearch untuk menawarkan kemampuan penelusuran dalam aplikasi kustom, yang memungkinkan pengguna menelusuri konten bahkan saat offline.

Diagram yang menggambarkan pengindeksan dan penelusuran dalam AppSearch

AppSearch menyediakan fitur berikut:

  • Implementasi penyimpanan yang cepat dan mengutamakan perangkat seluler dengan penggunaan I/O yang rendah
  • Pengindeksan dan pembuatan kueri yang sangat efisien pada set data besar
  • Dukungan multibahasa, seperti bahasa Inggris dan Spanyol
  • Peringkat relevansi dan penskoran penggunaan

Karena penggunaan I/O yang lebih rendah, AppSearch menawarkan latensi yang lebih rendah untuk pengindeksan dan penelusuran di seluruh set data besar dibandingkan dengan SQLite. AppSearch menyederhanakan kueri lintas jenis dengan mendukung kueri tunggal, sedangkan SQLite menggabungkan hasil dari beberapa tabel.

Untuk mengilustrasikan fitur AppSearch, mari kita ambil contoh aplikasi musik yang mengelola lagu favorit pengguna dan memungkinkan pengguna menelusurinya dengan mudah. Pengguna menikmati musik dari seluruh dunia dengan judul lagu dalam berbagai bahasa, yang secara native didukung oleh AppSearch untuk pengindeksan dan kueri. Saat pengguna menelusuri lagu berdasarkan judul atau nama artis, aplikasi cukup meneruskan permintaan ke AppSearch untuk mengambil lagu yang cocok dengan cepat dan efisien. Aplikasi menampilkan hasil, sehingga pengguna dapat mulai memutar lagu favorit mereka dengan cepat.

Penyiapan

Untuk menggunakan AppSearch di aplikasi Anda, tambahkan dependensi berikut ke file build.gradle aplikasi Anda:

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha05"

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

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

Konsep AppSearch

Diagram berikut menggambarkan konsep AppSearch dan interaksinya.

Garis besar
diagram aplikasi klien dan interaksinya dengan konsep
AppSearch berikut: database AppSearch, skema, jenis skema, dokumen,
sesi, dan penelusuran. Gambar 1. Diagram konsep AppSearch: database, skema, jenis skema, dokumen, sesi, dan penelusuran AppSearch.

Database dan sesi

Database AppSearch adalah kumpulan dokumen yang sesuai dengan skema database. Aplikasi klien membuat database dengan memberikan konteks aplikasi dan nama database. Database hanya dapat dibuka oleh aplikasi yang membuatnya. Saat database dibuka, sesi akan ditampilkan untuk berinteraksi dengan database. Sesi adalah titik entri untuk memanggil AppSearch API dan tetap terbuka hingga ditutup oleh aplikasi klien.

Skema dan jenis skema

Skema mewakili struktur organisasi data dalam database AppSearch.

Skema terdiri dari jenis skema yang mewakili jenis data unik. Jenis skema terdiri dari properti yang berisi nama, jenis data, dan kardinalitas. Setelah jenis skema ditambahkan ke skema database, dokumen jenis skema tersebut dapat dibuat dan ditambahkan ke database.

Dokumen

Di AppSearch, unit data direpresentasikan sebagai dokumen. Setiap dokumen dalam database AppSearch diidentifikasi secara unik berdasarkan namespace dan ID-nya. Namespace digunakan untuk memisahkan data dari berbagai sumber jika hanya satu sumber yang perlu dibuat kueri, seperti akun pengguna.

Dokumen berisi stempel waktu pembuatan, time-to-live (TTL), dan skor yang dapat digunakan untuk peringkat selama pengambilan. Dokumen juga diberi jenis skema yang menjelaskan properti data tambahan yang harus dimiliki dokumen.

Class dokumen adalah abstraksi dokumen. File ini berisi kolom beranotasi yang mewakili konten dokumen. Secara default, nama class dokumen menetapkan nama jenis skema.

Dokumen diindeks dan dapat ditelusuri dengan memberikan kueri. Dokumen akan cocok dan disertakan dalam hasil penelusuran jika berisi istilah dalam kueri atau cocok dengan spesifikasi penelusuran lain. Hasil diurutkan berdasarkan skor dan strategi peringkatnya. Hasil penelusuran diwakili oleh halaman yang dapat Anda ambil secara berurutan.

AppSearch menawarkan penyesuaian untuk penelusuran, seperti filter, konfigurasi ukuran halaman, dan pembuatan cuplikan.

Penyimpanan Platform, Penyimpanan Lokal, atau Penyimpanan Layanan Play

AppSearch menawarkan tiga solusi penyimpanan: LocalStorage, PlatformStorage, dan PlayServicesStorage. Dengan LocalStorage, aplikasi Anda mengelola indeks khusus aplikasi yang berada di direktori data aplikasi. Dengan PlatformStorage dan PlayServicesStorage, aplikasi Anda berkontribusi pada indeks pusat seluruh sistem. Indeks PlatformStorage dihosting di server sistem dan indeks PlayServicesStorage dihosting di penyimpanan Layanan Google Play. Akses data dalam indeks pusat ini dibatasi untuk data yang dikontribusikan oleh aplikasi Anda dan data yang telah dibagikan secara eksplisit kepada Anda oleh aplikasi lain. Semua opsi penyimpanan ini menggunakan API yang sama dan dapat ditukarkan berdasarkan versi perangkat:

Kotlin

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

Dengan menggunakan PlatformStorage dan PlayServicesStorage, aplikasi Anda dapat berbagi data secara aman dengan aplikasi lain agar aplikasi tersebut juga dapat menelusuri data aplikasi Anda. Berbagi data aplikasi hanya baca diberikan menggunakan handshake sertifikat untuk memastikan bahwa aplikasi lain memiliki izin untuk membaca data. Baca selengkapnya tentang API ini dalam dokumentasi untuk setSchemaTypeVisibilityForPackage().

Selain itu, dengan PlatformStorage, data yang diindeks dapat ditampilkan di platform UI Sistem. Aplikasi dapat memilih untuk tidak menampilkan beberapa atau semua datanya di platform UI Sistem. Baca selengkapnya tentang API ini dalam dokumentasi untuk setSchemaTypeDisplayedBySystem().

Fitur LocalStorage (kompatibel dengan Android 5.0+) PlatformStorage (kompatibel dengan Android 12+) PlayServicesStorage (kompatibel dengan Android 5.0+)
Penelusuran teks lengkap yang efisien
Dukungan multibahasa
Mengurangi ukuran biner
Berbagi data aplikasi ke aplikasi
Kemampuan untuk menampilkan data di platform UI Sistem
Ukuran dan jumlah dokumen yang diindeks tidak terbatas
Operasi yang lebih cepat tanpa latensi binder tambahan

Ada konsekuensi tambahan yang perlu dipertimbangkan saat memilih antara LocalStorage dan PlatformStorage. Karena PlatformStorage menggabungkan Jetpack API melalui layanan sistem AppSearch, dampak ukuran APK minimal dibandingkan dengan menggunakan LocalStorage. Namun, hal ini juga berarti operasi AppSearch menimbulkan latensi binder tambahan saat memanggil layanan sistem AppSearch. Dengan PlatformStorage, AppSearch membatasi jumlah dokumen dan ukuran dokumen yang dapat diindeks oleh aplikasi untuk memastikan indeks pusat yang efisien. PlayServicesStorage juga memiliki batasan yang sama dengan PlatformStorage dan hanya didukung di perangkat dengan layanan Google Play.

Mulai menggunakan AppSearch

Contoh di bagian ini menunjukkan cara menggunakan AppSearch API untuk berintegrasi dengan aplikasi pencatatan hipotetis.

Menulis class dokumen

Langkah pertama untuk berintegrasi dengan AppSearch adalah menulis class dokumen untuk menjelaskan data yang akan disisipkan ke dalam database. Tandai class sebagai class dokumen dengan menggunakan anotasi @Document.Anda dapat menggunakan instance class dokumen untuk memasukkan dokumen dan mengambil dokumen dari database.

Kode berikut menentukan class dokumen Catatan dengan kolom yang dianotasikan @Document.StringProperty untuk mengindeks teks objek Catatan.

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

Membuka database

Anda harus membuat database sebelum menggunakan dokumen. Kode berikut membuat database baru dengan nama notes_app dan mendapatkan ListenableFuture untuk AppSearchSession, yang mewakili koneksi ke database dan menyediakan API untuk operasi 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()
);

Menetapkan skema

Anda harus menetapkan skema sebelum dapat memasukkan dokumen dan mengambil dokumen dari database. Skema database terdiri dari berbagai jenis data terstruktur, yang disebut sebagai "jenis skema". Kode berikut menetapkan skema dengan memberikan class dokumen sebagai jenis skema.

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

Menempatkan dokumen di database

Setelah jenis skema ditambahkan, Anda dapat menambahkan dokumen jenis tersebut ke database. Kode berikut membuat dokumen jenis skema Note menggunakan builder class dokumen Note. Ini menetapkan namespace dokumen user1 untuk mewakili pengguna arbitrer dari sampel ini. Dokumen kemudian disisipkan ke dalam database dan pemroses dilampirkan untuk memproses hasil operasi 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);

Anda dapat menelusuri dokumen yang diindeks menggunakan operasi penelusuran yang dibahas di bagian ini. Kode berikut menjalankan kueri untuk istilah "buah" di database untuk dokumen yang termasuk dalam 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);

Melakukan iterasi melalui SearchResults

Penelusuran menampilkan instance SearchResults, yang memberikan akses ke halaman objek SearchResult. Setiap SearchResult menyimpan GenericDocument yang cocok, yaitu bentuk umum dokumen yang menjadi tujuan konversi semua dokumen. Kode berikut mendapatkan halaman pertama hasil penelusuran dan mengonversi hasilnya kembali menjadi dokumen 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);

Menghapus dokumen

Saat pengguna menghapus catatan, aplikasi akan menghapus dokumen Note yang sesuai dari database. Tindakan ini akan memastikan catatan tidak akan muncul lagi dalam kueri. Kode berikut membuat permintaan eksplisit untuk menghapus dokumen Note dari database menurut 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);

Mempertahankan ke disk

Update pada database harus disimpan secara berkala ke disk dengan memanggil requestFlush(). Kode berikut memanggil requestFlush() dengan pemroses untuk menentukan apakah panggilan berhasil.

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

Menutup sesi

AppSearchSession harus ditutup saat aplikasi tidak lagi memanggil operasi database apa pun. Kode berikut menutup sesi AppSearch yang dibuka sebelumnya dan mempertahankan semua update ke disk.

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

Referensi lainnya

Untuk mempelajari AppSearch lebih lanjut, lihat referensi tambahan berikut:

Contoh

  • Contoh Android AppSearch (Kotlin), aplikasi pencatat yang menggunakan AppSearch untuk mengindeks catatan pengguna dan memungkinkan pengguna menelusuri catatan mereka.

Berikan masukan

Sampaikan masukan dan ide Anda kepada kami melalui resource berikut:

Issue tracker

Laporkan bug agar kami dapat memperbaikinya.