AppSearch 是高效能的裝置端搜尋解決方案,可管理本機儲存的結構化資料。其中包含用於建立資料索引,以及透過全文搜尋擷取資料的 API。應用程式可以使用 AppSearch 提供自訂應用程式內搜尋功能,讓使用者即使在離線狀態下也能搜尋內容。
AppSearch 提供下列功能:
- 快速、行動優先的儲存空間實作,I/O 使用率低
- 可對大型資料集進行高效率的索引和查詢
- 支援多種語言,例如英文和西班牙文
- 關聯性排名和使用情形評分
由於 I/O 使用率較低,AppSearch 在大型資料集上建立索引和搜尋的延遲時間比 SQLite 更短。AppSearch 支援單一查詢,可簡化跨類型查詢,而 SQLite 則會合併多個資料表的結果。
為了說明 AppSearch 的功能,我們以音樂應用程式為例,該應用程式可管理使用者喜愛的歌曲,並讓使用者輕鬆搜尋。使用者可以欣賞來自世界各地的音樂,歌曲名稱也可能使用不同語言,而 AppSearch 原生支援這些語言的索引和查詢。當使用者依曲名或藝人名稱搜尋歌曲時,應用程式會直接將要求傳遞至 AppSearch,快速有效地擷取相符的歌曲。應用程式會顯示結果,讓使用者快速開始播放喜愛的歌曲。
設定
如要在應用程式中使用 AppSearch,請在應用程式的 build.gradle 檔案中新增下列依附元件:
Groovy
dependencies { def appsearch_version = "1.2.0-alpha02" 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 supports sharing data // with other apps. Feature support varies by device. Before calling an API that // requires a feature, check Features.isFeatureSupported(). implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version" // PlayServicesStorage is compatible with all devices that support Google Play Services on // all API levels. It offers similar capabilities to PlatformStorage and is the // recommended solution for lower API levels on which PlatformStorage is not supported. implementation "androidx.appsearch:appsearch-play-services-storage:$appsearch_version" }
Kotlin
dependencies { val appsearch_version = "1.2.0-alpha02" 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 supports sharing data // with other apps. Feature support varies by device. Before calling an API that // requires a feature, check Features.isFeatureSupported(). implementation("androidx.appsearch:appsearch-platform-storage:$appsearch_version") // PlayServicesStorage is compatible with all devices that support Google Play Services on // all API levels. It offers similar capabilities to PlatformStorage and is the // recommended solution for lower API levels on which PlatformStorage is not supported. implementation("androidx.appsearch:appsearch-play-services-storage:$appsearch_version") }
AppSearch 概念
下圖說明 AppSearch 概念及其互動。
圖 1. AppSearch 概念圖:AppSearch 資料庫、結構定義、結構定義類型、文件、工作階段和搜尋。
資料庫和工作階段
AppSearch 資料庫是符合資料庫結構定義的文件集合。用戶端應用程式會提供應用程式內容和資料庫名稱,藉此建立資料庫。資料庫只能由建立資料庫的應用程式開啟。開啟資料庫時,系統會傳回工作階段,以便與資料庫互動。工作階段是呼叫 AppSearch API 的進入點,會保持開啟狀態,直到用戶端應用程式關閉為止。
結構定義和結構定義類型
結構定義代表 AppSearch 資料庫中資料的組織結構。
結構定義由結構定義類型組成,代表不重複的資料類型。結構定義類型包含屬性,屬性則包含名稱、資料類型和基數。將結構定義類型新增至資料庫結構定義後,即可建立該結構定義類型的文件,並新增至資料庫。
文件
在 AppSearch 中,資料單元會以文件形式呈現。AppSearch 資料庫中的每份文件都由其命名空間和 ID 唯一識別。如果只需要查詢一個來源 (例如使用者帳戶),命名空間可用來分隔不同來源的資料。
文件包含建立時間戳記、存留時間 (TTL) 和分數,可用於在擷取期間進行排名。系統也會為文件指派結構定義類型,說明文件必須具備的其他資料屬性。
文件類別是文件的抽象概念。其中包含代表文件內容的註解欄位。根據預設,文件類別的名稱會設定結構定義類型的名稱。
搜尋
文件會建立索引,並可透過查詢進行搜尋。如果文件包含查詢中的字詞,或符合其他搜尋規格,就會比對並納入搜尋結果。結果會根據分數和排名策略排序。搜尋結果會以頁面呈現,您可以依序擷取。
AppSearch 提供搜尋自訂功能,例如篩選器、頁面大小設定和摘要。
平台儲存空間、本機儲存空間或 Play 服務儲存空間
AppSearch 提供三種儲存空間解決方案:LocalStorage、PlatformStorage 和 PlayServicesStorage。使用 LocalStorage 時,應用程式會管理位於應用程式資料目錄中的應用程式專屬索引。透過 PlatformStorage 和 PlayServicesStorage,應用程式會提供系統層級的中央索引。PlatformStorage 的索引託管於系統伺服器,PlayServicesStorage 的索引則託管於 Google Play 服務的儲存空間。在這些中央索引中,資料存取權僅限於應用程式提供的資料,以及其他應用程式明確與您共用的資料。這些儲存空間選項都使用相同的 API,且可根據裝置版本進行切換。
請注意,功能支援情形會因儲存空間選項和裝置而異。詳情請參閱「查看功能可用性」。
下列程式碼會在 Android 12 (API 級別 31) 以上版本建立 PlatformStorage 工作階段,並在舊版建立 PlayServicesStorage 或 LocalStorage 工作階段:
Kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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())); } }
應用程式可使用 PlatformStorage 和 PlayServicesStorage,安全地與其他應用程式共用資料,讓其他應用程式也能搜尋您的應用程式資料。系統會使用憑證交握授予唯讀應用程式資料共用權限,確保其他應用程式有權讀取資料。如要進一步瞭解這個 API,請參閱 setSchemaTypeVisibilityForPackage() 的說明文件。
此外,透過 PlatformStorage,已建立索引的資料可以顯示在系統 UI 介面上。應用程式可以選擇不讓部分或所有資料顯示在系統 UI 介面上。如要進一步瞭解這個 API,請參閱 setSchemaTypeDisplayedBySystem() 的說明文件。
| 功能 | LocalStorage (與 Android 5.0 以上版本相容) |
PlatformStorage (與 Android 12 以上版本相容) |
PlayServicesStorage (與 Android 5.0 以上版本相容) |
|---|---|---|---|
| 高效全文搜尋 | |||
| 支援多個語言 | |||
| 縮減二進位檔大小 | |||
| 應用程式間的資料共用 | |||
| 可在系統 UI 介面顯示資料 | |||
| 可建立索引的文件大小和數量不受限制 | |||
| 作業速度更快,不會有額外的繫結器延遲 |
在 LocalStorage 和 PlatformStorage 之間選擇時,還需要考量其他取捨。由於 PlatformStorage 會透過 AppSearch 系統服務包裝 Jetpack API,因此與使用 LocalStorage 相比,對 APK 大小的影響極小。不過,這也代表 AppSearch 作業在呼叫 AppSearch 系統服務時,會產生額外的繫結器延遲時間。透過 PlatformStorage,AppSearch 會限制應用程式可建立索引的文件數量和大小,確保中央索引有效率。PlayServicesStorage 的限制與 PlatformStorage 相同,且僅支援搭載 Google Play 服務的裝置。
各項功能適用的國家/地區
雖然所有儲存空間選項都使用相同的 API,但部分功能不適用於所有儲存空間選項或裝置。在 API 參考資料中,@RequiresFeature 註解會識別需要其中一項功能的 API,而 Features 類別會為每項功能定義常數。例如,INDEXING_TYPE_RANGE 需要 Features.NUMERIC_SEARCH。
如果是 PlatformStorage,功能支援情形取決於裝置的 Android 版本。自 Android 13 (API 級別 33) 起,功能支援也取決於裝置收到的 Google Play 系統更新。舉例來說,PlatformStorage 僅在下列裝置上支援數字搜尋 (Features.NUMERIC_SEARCH)、逐字搜尋 (Features.VERBATIM_SEARCH) 和聯結 (Features.JOIN_SPEC_AND_QUALIFIED_ID):
- 搭載 Android 14 (API 級別 34) 以上版本的裝置。
- 搭載 Android 13 (API 級別 33) 且已收到必要 Google Play 系統更新的裝置。
搭載 Android 12 或 Android 12L (API 級別 31 和 32) 的裝置不支援這些功能。
文件中已建立索引的屬性數量上限,也會因裝置和儲存空間選項而異。如要在執行階段取得限制,請呼叫 getMaxIndexedProperties()。
請勿僅依據 Android 版本判斷是否支援某項功能。呼叫需要某項功能的 API 之前,請先在 AppSearchSession.getFeatures() 傳回的 Features 物件上呼叫 isFeatureSupported(),確認是否支援該功能。如果系統不支援必要功能,作業就會失敗並發生例外狀況。舉例來說,如果設定的結構定義包含 INDEXING_TYPE_RANGE 屬性,在不支援 Features.NUMERIC_SEARCH 的裝置上就會失敗。
下列程式碼會檢查開放式 AppSearchSession 是否支援數字搜尋:
Kotlin
val features = session.features if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) { // Numeric search is supported. You can index properties with // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with // SearchSpec.Builder.setNumericSearchEnabled(true). } else { // Numeric search isn't supported. Set a schema without range-indexed // properties, or switch to a different storage option. }
Java
Features features = session.getFeatures(); if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) { // Numeric search is supported. You can index properties with // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with // SearchSpec.Builder.setNumericSearchEnabled(true). } else { // Numeric search isn't supported. Set a schema without range-indexed // properties, or switch to a different storage option. }
開始使用 AppSearch
本節範例說明如何使用 AppSearch API 整合假設的記事應用程式。
編寫文件類別
與 AppSearch 整合的第一步是編寫文件類別,說明要插入資料庫的資料。使用 @Document 註解將類別標示為文件類別。您可以使用文件類別的例項,將文件放入資料庫及從資料庫擷取文件。
下列程式碼定義了 Note 文件類別,其中包含 @Document.StringProperty 註解欄位,用於為 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; } }
開啟資料庫
您必須先建立資料庫,才能處理文件。下列程式碼會建立名為 notes_app 的新資料庫,並取得 AppSearchSession 的 ListenableFuture,代表與資料庫的連線,並提供資料庫作業的 API。
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() );
設定結構定義
您必須先設定結構定義,才能在資料庫中放入及擷取文件。資料庫結構定義包含不同類型的結構化資料,稱為「結構定義類型」。下列程式碼會提供文件類別做為結構定義類型,藉此設定結構定義。
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);
將文件放入資料庫
新增結構定義類型後,即可將該類型的文件新增至資料庫。下列程式碼會使用 Note 文件類別建構工具,建構結構定義類型為 Note 的文件。它會將文件命名空間 user1 設定為代表這個範例的任意使用者。接著,文件會插入資料庫,並附加監聽器來處理放置作業的結果。
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);
搜尋
您可以使用本節介紹的搜尋作業,搜尋已建立索引的文件。下列程式碼會查詢資料庫中屬於 user1 命名空間的文件,找出「fruit」一詞。
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);
疊代 SearchResults
搜尋會傳回 SearchResults 執行個體,可讓您存取 SearchResult 物件的頁面。每個 SearchResult 都會保留相符的 GenericDocument,這是所有文件都會轉換成的一般文件形式。下列程式碼會取得搜尋結果的第一頁,並將結果轉換回 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);
移除文件
使用者刪除記事時,應用程式會從資料庫中刪除對應的 Note 文件。這樣一來,查詢結果就不會再顯示該記事。下列程式碼會明確要求依 ID 從資料庫中移除 Note 文件。
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);
保存至磁碟
資料庫更新應定期呼叫 requestFlush(),將資料保存至磁碟。下列程式碼會使用事件監聽器呼叫 requestFlush(),判斷呼叫是否成功。
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);
關閉工作階段
當應用程式不再呼叫任何資料庫作業時,應關閉 AppSearchSession。下列程式碼會關閉先前開啟的 AppSearch 工作階段,並將所有更新內容保存至磁碟。
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);
其他資源
如要進一步瞭解 AppSearch,請參閱下列其他資源:
範例
- Android AppSearch 範例 (Kotlin):這款筆記應用程式使用 AppSearch 為使用者的筆記建立索引,並允許使用者搜尋筆記。
提供意見
歡迎透過下列資源與我們分享意見和想法:
回報錯誤,協助我們修正問題。