AppSearch 是高效能的裝置端搜尋解決方案,可用來管理本機儲存的結構化資料。其中包含的 API 可用於將資料編入索引,以及透過全文搜尋擷取資料。應用程式可以使用 AppSearch 提供自訂應用程式內搜尋功能,讓使用者即使離線也能搜尋內容。

AppSearch 提供下列功能:
- 快速的行動裝置優先儲存空間實作,低 I/O 用量低
- 高效率建立大型資料集的索引與查詢作業
- 支援多種語言,例如英文和西班牙文
- 關聯性排名和使用分數
由於 I/O 使用較少,與 SQLite 相比,AppSearch 縮短針對大型資料集建立索引和搜尋的延遲時間。AppSearch 支援單一查詢,而 SQLite 會合併多個資料表的結果,藉此簡化跨類型查詢。
我們以音樂應用程式為例,來說明 AppSearch 的功能,讓使用者輕鬆搜尋這些音樂應用程式。使用者喜歡使用不同語言的歌曲名稱,進而享受世界各地的音樂,而 AppSearch 原生支援將歌曲編入索引和查詢功能。當使用者依名稱或藝人名稱搜尋歌曲時,應用程式只會將要求傳送至 AppSearch,以便快速有效地擷取相符的歌曲。應用程式會顯示結果,讓使用者快速開始播放喜愛的歌曲。
設定
如要在應用程式中使用 AppSearch,請將下列依附元件新增至應用程式的 build.gradle
檔案:
Groovy
dependencies { def appsearch_version = "1.1.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 offers additional features // to LocalStorage. implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version" }
Kotlin
dependencies { val appsearch_version = "1.1.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 offers additional features // to LocalStorage. implementation("androidx.appsearch:appsearch-platform-storage:$appsearch_version") }
AppSearch 概念
下圖說明 AppSearch 概念及其互動情形。
圖 1. AppSearch 概念圖表:AppSearch 資料庫、結構定義、結構定義類型、文件、工作階段和搜尋。
資料庫和工作階段
AppSearch 資料庫是符合資料庫結構定義的一系列文件。用戶端應用程式會提供應用程式結構定義和資料庫名稱來建立資料庫。只有建立資料庫的應用程式才能開啟資料庫。開啟資料庫時,系統會傳回工作階段以與資料庫互動。這個工作階段是呼叫 AppSearch API 的進入點,會保持開啟狀態,直到用戶端應用程式關閉為止。
結構定義與結構定義類型
結構定義代表 AppSearch 資料庫內的資料機構結構。
結構定義由代表獨特資料類型的結構定義類型組成。結構定義類型由包含名稱、資料類型和基數的屬性組成。將結構定義類型新增至資料庫結構定義後,就可以建立該結構定義類型的文件並新增至資料庫。
文件
在 AppSearch 中,資料單位會以文件表示。AppSearch 資料庫中的每份文件都會以其命名空間和 ID 做為唯一識別依據。如果只需要查詢一個來源 (例如使用者帳戶),命名空間可用來分隔不同來源的資料。
文件包含建立時間戳記、存留時間 (TTL) 和分數,可用於擷取期間的排名。也會為文件指派結構定義類型,說明文件必須具備的其他資料屬性。
文件類別是文件的抽象層。其中包含代表文件內容的註解欄位。根據預設,文件類別名稱會設定結構定義類型的名稱。
搜尋
文件已建立索引,可透過提供查詢來搜尋。如果文件包含查詢中的字詞,或是符合其他搜尋規格,則文件會比對成功,並納入搜尋結果中。結果會根據分數和排名策略排序。搜尋結果會以您可以依序擷取的網頁呈現。
AppSearch 提供搜尋的自訂功能,例如篩選器、頁面大小設定和摘要。
平台儲存空間與本機儲存空間
AppSearch 提供兩種儲存空間解決方案:LocalStorage 和 PlatformStorage。透過 LocalStorage,應用程式可管理位於應用程式資料目錄中的應用程式專屬索引。使用 PlatformStorage,您的應用程式就能構成整個系統的中央索引。中央索引中的資料存取權僅限於您的應用程式提供的資料,以及其他應用程式明確與您分享的資料。LocalStorage 和 PlatformStorage 都共用相同的 API,且可以依據裝置版本互換:
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())); }
使用 PlatformStorage,您的應用程式可以安全地與其他應用程式共用資料,讓這些應用程式也能搜尋您的應用程式資料。系統會透過憑證握手授予唯讀應用程式資料共用權限,確保其他應用程式具有讀取資料的權限。如要進一步瞭解這個 API,請參閱 setSchemaTypeVisibilityForPackage() 的說明文件。
此外,已建立索引的資料也會顯示在系統 UI 介面上。 應用程式可選擇不要在系統 UI 介面上顯示部分或所有資料。如要進一步瞭解這個 API,請參閱 setSchemaTypeDisplayedBySystem() 的說明文件。
功能 | 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 |
選擇 LocalStorage 和 PlatformStorage 時,必須考量其他的權衡取捨。由於 PlatformStorage 會包裝 Jetpack API 而非 AppSearch 系統服務,因此比起 LocalStorage,APK 大小的影響不大。不過,這也表示 AppSearch 作業在呼叫 AppSearch 系統服務時,會產生額外的繫結器延遲。在 PlatformStorage 中,AppSearch 會限制應用程式可建立索引的文件數量和文件大小,以確保中央索引高效。
開始使用 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
,代表此範例的任意使用者。接著,文件會插入資料庫,還會附加一個事件監聽器以處理 Place 作業的結果。
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 為使用者的筆記建立索引,並讓使用者搜尋自己的記事。
提供意見
歡迎透過下列資源與我們分享意見和想法:
回報錯誤,幫助我們進行修正。