AppSearch

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

Diagram illustrating indexing and searching within AppSearch

AppSearch 提供以下功能

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

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

为了说明 AppSearch 的功能,让我们以一个管理用户最喜欢的歌曲并允许用户轻松搜索这些歌曲的音乐应用程序为例。用户喜欢来自世界各地的音乐,歌曲标题使用不同的语言,AppSearch 本地支持对这些语言进行索引和查询。当用户按标题或艺术家姓名搜索歌曲时,应用程序只需将请求传递给 AppSearch 以快速有效地检索匹配的歌曲。应用程序呈现结果,允许其用户快速开始播放他们最喜欢的歌曲。

设置

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

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha05"

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

    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(与 Android 4.0+ 兼容) PlatformStorage(与 Android 12+ 兼容)
高效的全文搜索
支持多种语言
减少二进制文件大小
应用程序到应用程序的数据共享
能够在系统 UI 表面上显示数据
可以索引无限的文档大小和数量
没有额外的绑定器延迟,操作更快

在选择 LocalStorage 和 PlatformStorage 之间时,需要考虑其他权衡。由于 PlatformStorage 在 AppSearch 系统服务之上包装了 Jetpack API,因此与使用 LocalStorage 相比,APK 大小影响很小。但是,这也意味着 AppSearch 操作在调用 AppSearch 系统服务时会产生额外的绑定器延迟。使用 PlatformStorage,AppSearch 会限制应用程序可以索引的文档数量和文档大小,以确保高效的中央索引。

开始使用 AppSearch

本节中的示例展示了如何使用 AppSearch API 与假设的笔记应用程序集成。

编写文档类

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

以下代码定义了一个带有 @Document.StringProperty 注释字段的 Note 文档类,用于索引 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()
);

设置模式

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

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

您可以搜索使用本节中介绍的搜索操作索引的文档。以下代码对数据库中属于 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 的更多信息,请参阅以下其他资源

示例

提供反馈

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

问题跟踪器

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