AppSearch

AppSearch 是一种高性能的设备端搜索解决方案,用于管理本地存储的结构化数据。它包含用于索引数据和使用全文搜索检索数据的 API。应用可以使用 AppSearch 提供自定义的应用内搜索功能,让用户即使离线也能搜索内容。

Diagram illustrating indexing and searching within AppSearch

AppSearch 提供以下功能

  • 快速、移动设备优先的存储实现,I/O 使用率低
  • 对大型数据集进行高效的索引和查询
  • 支持多种语言,如英语和西班牙语
  • 相关性排名和使用评分

与 SQLite 相比,由于 I/O 使用率较低,AppSearch 在对大型数据集进行索引和搜索时提供了更低的延迟。AppSearch 通过支持单次查询简化了跨类型查询,而 SQLite 合并了多个表的结果。

为了说明 AppSearch 的功能,我们以一个音乐应用为例,该应用管理用户的收藏歌曲并允许用户轻松搜索它们。用户可以欣赏来自世界各地的音乐,歌曲标题采用不同语言,AppSearch 原生支持对这些语言进行索引和查询。当用户按歌曲标题或艺术家姓名搜索歌曲时,应用只需将请求传递给 AppSearch,即可快速高效地检索匹配的歌曲。应用会显示结果,让用户能够快速开始播放他们喜爱的歌曲。

设置

要在您的应用中使用 AppSearch,请将以下依赖项添加到您应用的 build.gradle 文件中

Groovy

dependencies {
    def appsearch_version = "1.1.0-rc01"

    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"

    // PlayServicesStorage is compatible with all devices that support Google Play Services on
    // all API levels. It offers the same features as 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.1.0-rc01"

    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")

    // PlayServicesStorage is compatible with all devices that support Google Play Services on
    // all API levels. It offers the same features as 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 数据库、Schema、Schema 类型、文档、Session 和搜索。 图 1. AppSearch 概念图:AppSearch 数据库、Schema、Schema 类型、文档、Session 和搜索。

数据库和 Session

AppSearch 数据库是符合数据库 Schema 的文档集合。客户端应用通过提供其应用上下文和数据库名称来创建数据库。数据库只能由创建它的应用打开。打开数据库后,会返回一个 Session 来与数据库交互。Session 是调用 AppSearch API 的入口点,并保持打开状态,直到客户端应用将其关闭。

Schema 和 Schema 类型

Schema 表示 AppSearch 数据库中数据的组织结构。

Schema 由表示独特数据类型的 Schema 类型组成。Schema 类型包含具有名称、数据类型和基数的属性。一旦将 Schema 类型添加到数据库 Schema 中,就可以创建该 Schema 类型的文档并将其添加到数据库中。

文档

在 AppSearch 中,一个数据单元表示为一个文档。AppSearch 数据库中的每个文档都由其命名空间和 ID 唯一标识。命名空间用于在只需要查询一个来源(如用户帐户)时,分隔来自不同来源的数据。

文档包含创建时间戳、存活时间 (TTL) 和分数,分数可用于检索期间的排名。文档还被分配一个 Schema 类型,该类型描述了文档必须拥有的其他数据属性。

文档类是文档的抽象。它包含表示文档内容的带注解字段。默认情况下,文档类的名称设置 Schema 类型的名称。

文档会被索引,并且可以通过提供查询来搜索。如果文档包含查询中的术语或匹配其他搜索规范,则会被匹配并包含在搜索结果中。结果根据其分数和排名策略进行排序。搜索结果以页面形式表示,您可以按顺序检索这些页面。

AppSearch 提供搜索的自定义选项,例如过滤器、页面大小配置和代码段摘录。

平台存储、本地存储或 Play 服务存储

AppSearch 提供三种存储解决方案:LocalStoragePlatformStoragePlayServicesStorage。使用 LocalStorage,您的应用管理位于应用数据目录中的应用特定索引。使用 PlatformStoragePlayServicesStorage,您的应用为系统范围的中心索引做出贡献。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()));
    }
}

使用 PlatformStoragePlayServicesStorage,您的应用可以安全地与其他应用共享数据,从而允许它们也搜索您的应用数据。只读应用数据共享通过证书握手授权,以确保其他应用有权读取数据。在 setSchemaTypeVisibilityForPackage() 的文档中详细了解此 API。

此外,使用 PlatformStorage,可以将在 System UI 界面上显示索引数据。应用可以选择不将其部分或全部数据显示在 System UI 界面上。在 setSchemaTypeDisplayedBySystem() 的文档中详细了解此 API。

功能 LocalStorage(兼容 Android 5.0+) PlatformStorage(兼容 Android 12+) PlayServicesStorage(兼容 Android 5.0+)
高效的全文搜索
多语言支持
减小的二进制大小
应用间数据共享
在 System UI 界面上显示数据的功能
可索引无限数量和大小的文档
操作更快,没有额外的绑定器延迟

在选择 LocalStoragePlatformStorage 时,还有一些额外的权衡需要考虑。由于 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 的新数据库,并获取了一个 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()
);

设置 Schema

在数据库中放入和检索文档之前,您必须设置 Schema。数据库 Schema 由不同类型的结构化数据组成,称为“Schema 类型”。以下代码通过提供文档类作为 Schema 类型来设置 Schema。

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);

在数据库中放入文档

一旦添加了 Schema 类型,您就可以向数据库添加该类型的文档。以下代码使用 Note 文档类构建器构建 Schema 类型为 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);

关闭 Session

当应用不再调用任何数据库操作时,应关闭 AppSearchSession。以下代码关闭之前打开的 AppSearch Session,并将所有更新持久化到磁盘。

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 的更多信息,请参阅以下其他资源

示例

提供反馈

通过这些资源与我们分享您的反馈和想法

问题跟踪器

报告错误,以便我们修复。