AppSearch

AppSearch は、ローカルに保存された構造化データを管理するための高性能なオンデバイス検索ソリューションです。全文検索を使用してデータをインデックス登録し、データを取得するための API が含まれています。アプリは AppSearch を使用してカスタムのアプリ内検索機能を提供し、ユーザーがオフラインでもコンテンツを検索できるようになります。

AppSearch 内でのインデックス登録と検索の図

AppSearch には次の機能があります。

  • I/O 使用率の低い、高速かつモバイル ファーストなストレージの実装
  • 大規模なデータセットに対する効率性に優れたインデックス登録とクエリ
  • 複数言語のサポート(英語やスペイン語など)
  • 関連性ランキングと使用状況スコア

AppSearch は I/O 使用量が少ないため、大規模なデータセットの場合、SQLite に比べてインデックス作成と検索のレイテンシが低くなります。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 によって一意に識別されます。名前空間は、ユーザー アカウントなど、1 つのソースのみをクエリする必要がある場合に、異なるソースのデータを分離するために使用されます。

ドキュメントには、作成タイムスタンプ、有効期間(TTL)、取得時のランキングに使用できるスコアが含まれます。ドキュメントにはスキーマタイプも割り当てられます。これは、ドキュメントに必要な追加のデータ プロパティを記述するものです。

ドキュメント クラスはドキュメントを抽象化したものです。これには、ドキュメントの内容を表すアノテーション付きフィールドが含まれます。デフォルトでは、ドキュメント クラスの名前がスキーマタイプの名前を設定します。

ドキュメントはインデックスに登録され、クエリを入力することで検索できます。クエリにキーワードを含むドキュメントや、別の検索仕様と一致するドキュメントは、照合され検索結果に表示されます。結果は、スコアとランキング戦略に基づいて並べ替えられます。検索結果は、順次取得できるページで表されます。

AppSearch は、フィルタ、ページサイズの構成、スニペットなど、検索のカスタマイズに対応しています。

プラットフォーム ストレージとローカル ストレージ

AppSearch には、LocalStorage と PlatformStorage の 2 つのストレージ ソリューションが用意されています。 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 を使用すると、アプリは他のアプリとデータを安全に共有して、他のアプリもアプリのデータを検索できるようになります。読み取り専用のアプリデータ共有は、証明書 handshake によって付与されるもので、他のアプリにデータを読み取る権限があることを保証します。この 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 アノテーションを使用して、クラスをドキュメント クラスとしてマークします。ドキュメント クラスのインスタンスを使用して、ドキュメントを挿入し、データベースからドキュメントを取得できます。

次のコードは、メモ オブジェクトのテキストをインデックス登録するために、@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 を設定します。ドキュメントはデータベースに挿入され、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 について詳しくは、以下の参考情報をご覧ください。

サンプル

フィードバックを送信

以下のリソースを通じてフィードバックやアイデアをお寄せください。

Issue Tracker

Google が修正できるよう、バグを報告します。