AppSearch 是高效能的裝置端搜尋解決方案,可管理本機儲存的結構化資料。其中包含 API,可用於將資料編入索引,並透過全文搜尋擷取資料。應用程式可以使用 AppSearch 提供自訂應用程式內搜尋功能,讓使用者即使在離線時也能搜尋內容。
![說明圖:說明在 AppSearch 中建立索引和搜尋](https://developer.android.google.cn/static/images/guide/topics/search/appsearch.png?authuser=5&hl=zh-tw)
AppSearch 提供下列功能:
- 快速的行動優先儲存空間實作方式,可降低 I/O 用量
- 針對大型資料集提供高效率的索引和查詢功能
- 支援多種語言,例如英文和西班牙文
- 關聯性排名和使用分數
由於 I/O 使用量較低,AppSearch 在索引和搜尋大型資料集時的延遲時間,相較於 SQLite 會更短。AppSearch 支援單一查詢,因此可簡化跨類型查詢,而 SQLite 則會合併多個資料表的結果。
為說明 AppSearch 的功能,我們以音樂應用程式為例,該應用程式可管理使用者喜愛的歌曲,並讓使用者輕鬆搜尋歌曲。使用者會收聽來自世界各地的音樂,歌曲名稱則以不同語言呈現,而 AppSearch 本身支援建立索引和查詢這些名稱。當使用者依歌名或藝人名稱搜尋歌曲時,應用程式只需將要求傳遞至 AppSearch,即可快速且有效率地擷取相符的歌曲。應用程式會顯示結果,讓使用者快速開始播放喜愛的歌曲。
設定
如要在應用程式中使用 AppSearch,請在應用程式的 build.gradle
檔案中新增下列依附元件:
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") }
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,且可根據裝置版本互換:
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())); } }
使用 PlatformStorage
和 PlayServicesStorage
,應用程式就能安全地與其他應用程式共用資料,讓這些應用程式也能搜尋您的應用程式資料。系統會使用憑證握手來授予唯讀應用程式資料分享權限,確保其他應用程式有權讀取資料。如要進一步瞭解這個 API,請參閱 setSchemaTypeVisibilityForPackage()
的說明文件。
此外,有了 PlatformStorage
,系統 UI 途徑就能顯示已編入索引的資料。應用程式可以選擇不讓部分或全部資料顯示在系統 UI 途徑上。如要進一步瞭解這個 API,請參閱 setSchemaTypeDisplayedBySystem()
的說明文件。
功能 | LocalStorage (相容於 Android 5.0 以上版本) |
PlatformStorage (相容於 Android 12 以上版本) |
PlayServicesStorage (相容於 Android 5.0 以上版本) |
---|---|---|---|
高效率的全文搜尋 | |||
支援多種語言 | |||
縮減二進位檔大小 | |||
應用程式間的資料共用 | |||
在系統 UI 途徑上顯示資料的能力 | |||
可建立無限數量的文件索引 | |||
加快作業速度,且不會增加 Binder 延遲時間 |
在選擇 LocalStorage
和 PlatformStorage
時,還需要考量其他取捨。由於 PlatformStorage
會在 AppSearch 系統服務上包裝 Jetpack API,因此與使用 LocalStorage 相比,APK 大小的影響最小。不過,這也表示 AppSearch 作業在呼叫 AppSearch 系統服務時,會產生額外的繫結器延遲時間。透過 PlatformStorage
,AppSearch 會限制應用程式可索引的文件數量和文件大小,確保中央索引的效率。PlayServicesStorage
也具有與 PlatformStorage
相同的限制,且僅支援搭載 Google Play 服務的裝置。
開始使用 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
設為代表此範例的任意使用者。接著,系統會將文件插入資料庫,並附加監聽器來處理 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);
搜尋
您可以使用本節所述的搜尋作業,搜尋已編入索引的文件。以下程式碼會針對屬於 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 為使用者的記事建立索引,並允許使用者搜尋自己的記事。
提供意見
歡迎透過下列資源與我們分享意見和想法:
回報錯誤,協助我們修正錯誤。