AppSearch

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

說明 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 資料庫、結構定義、結構定義類型、文件、工作階段和搜尋。 圖 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 是以 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 的新資料庫,並取得 AppSearchSessionListenableFuture,該資料庫代表與資料庫的連線,並提供資料庫作業的 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,請參閱下列其他資源:

範例

提供意見

歡迎透過下列資源與我們分享意見和想法:

Issue Tracker

回報錯誤,協助我們修正錯誤。