AppSearch

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

圖表: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 概念及其互動。

用戶端應用程式的圖表大綱,以及與下列 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,請參閱下列其他資源:

範例

提供意見

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

Issue Tracker

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