AppSearch

AppSearch 是一种高性能的设备端搜索解决方案,用于在本地进行管理 结构化数据。它包含用于将数据编入索引和检索数据的 API 搜索。应用可以使用 AppSearch 提供自定义应用内 搜索功能,使用户即使在离线状态下也能搜索内容。

在 AppSearch 中编入索引和进行搜索的示意图

AppSearch 提供以下功能:

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

由于 I/O 使用量较少,AppSearch 可缩短编入索引和搜索的延迟时间 大型数据集处理能力的提升。AppSearch 可简化跨类型查询 而 SQLite 会合并多个表中的结果。

为了说明 AppSearch 的功能,我们以音乐为例来说明 该应用管理用户喜爱的歌曲,并允许用户轻松搜索 。用户欣赏来自世界各地的音乐,歌曲名称各不相同 语言,AppSearch 原生支持编入索引和查询。当 当用户按歌名或音乐人姓名搜索歌曲时,应用只需传递 向 AppSearch 发出的请求,以快速、高效地检索匹配的歌曲。通过 应用会显示结果,让用户能够快速开始游戏 和喜爱的歌曲

设置

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

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha07"

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

    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() 文档)。

此外,已编入索引的数据可能会显示在系统界面 surface 上。 应用程序可以选择不在“系统”上显示其部分或全部数据 界面 surface。如需详细了解此 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 系统服务,与使用 LocalStorage 类。不过,这也意味着 AppSearch 操作 调用 AppSearch 系统服务时 binder 延迟时间。借助 PlatformStorage AppSearch 对应用的文档数量和文档大小有限制 以确保高效的中央索引

AppSearch 使用入门

本部分中的示例展示了如何使用 AppSearch API 来集成 一个假想的记事应用

编写文档类

与 AppSearch 集成的第一步是编写一个文档类, 描述要插入数据库的数据。将类标记为文档类 使用 @Document 您可以使用文档类的实例将文档放入和 从数据库中检索文档。

以下代码定义了一个带有 @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 的新数据库,并获取 ListenableFutureAppSearchSession), 它代表与数据库的连接,并提供用于 数据库操作。

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

您可以使用下文介绍的搜索操作来搜索已编入索引的文档, 部分。以下代码对字词“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

搜索会返回 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 提取一个文档。这样可确保备注不再显示在 查询。以下代码明确发出了移除 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,请参阅下面列出的其他资源:

示例

提供反馈

通过以下资源与我们分享您的反馈和想法:

问题跟踪器

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