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-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") }
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 是以 AppSearch 系統服務來包裝 Jetpack API,因此相較於使用 LocalStorage,APK 大小影響最小。然而,這也表示在呼叫 AppSearch 系統服務時,AppSearch 作業會產生額外的繫結器延遲時間。使用 PlatformStorage,AppSearch 會限制應用程式可建立索引的文件數量和文件大小,確保中央索引保持效率。
開始使用 AppSearch
本節中的範例說明如何使用 AppSearch API,與假設的記事應用程式整合。
撰寫文件類別
整合 AppSearch 的第一步,是編寫文件類別來說明要插入資料庫的資料。使用 @Document
註解將類別標示為文件類別。您可以使用文件類別的執行個體將文件放入其中,並從資料庫中擷取文件。
以下程式碼定義了 Note 文件類別,其中包含 @Document.StringProperty
註解欄位,用於建立索引記事物件的文字。
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);
搜尋
您可以使用本節涵蓋的搜尋作業,搜尋已編入索引的文件。下列程式碼會執行資料庫「fruit」這個字詞,針對屬於 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);
透過搜尋結果反覆執行
搜尋會傳回 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 為使用者的筆記建立索引,並讓使用者搜尋其筆記。
提供意見
歡迎透過下列資源與我們分享意見和想法:
回報錯誤,協助我們修正錯誤。