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.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 によって一意に識別されます。名前空間は、ユーザー アカウントなど、1 つのソースのみをクエリする必要がある場合に、異なるソースのデータの分離に使用されます。

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

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

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

AppSearch では、フィルタ、ページサイズの構成、スニペットなど、検索のカスタマイズが可能です。

プラットフォーム ストレージ、ローカル ストレージ、Play 開発者サービス ストレージ

AppSearch には、LocalStoragePlatformStoragePlayServicesStorage の 3 つのストレージ ソリューションが用意されています。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 を使用すると、アプリは他のアプリとデータを安全に共有して、他のアプリがアプリのデータも検索できるようにすることができます。読み取り専用アプリケーション データの共有は、証明書ハンドシェイクを使用して付与され、他のアプリにデータの読み取り権限が付与されます。この API の詳細については、setSchemaTypeVisibilityForPackage() のドキュメントをご覧ください。

また、PlatformStorage を使用すると、インデックスに登録されたデータをシステム UI サーフェスに表示できます。アプリケーションは、システム UI サーフェスに表示されるデータの一部またはすべてをオプトアウトできます。この API の詳細については、setSchemaTypeDisplayedBySystem() のドキュメントをご覧ください。

機能 LocalStorage(Android 5.0 以降に対応) PlatformStorage(Android 12 以降に対応) PlayServicesStorage(Android 5.0 以降に対応)
効率的な全文検索
多言語対応
バイナリサイズの削減
アプリケーション間のデータ共有
システム UI サーフェスにデータを表示する機能
ドキュメントのサイズと数に上限なし
バインダーのレイテンシを増やさずにオペレーションを高速化

LocalStoragePlatformStorage のどちらを選択するかを決める際には、考慮すべきトレードオフが他にもあります。PlatformStorage は AppSearch システム サービスで Jetpack API をラップするため、APK サイズへの影響は LocalStorage を使用する場合と比べて最小限です。ただし、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()
);

スキーマを設定する

データベースにドキュメントを保存したり、データベースからドキュメントを取得したりするには、スキーマを設定する必要があります。データベース スキーマは、さまざまなタイプの構造化データ(スキーマタイプ)で構成されています。次のコードは、ドキュメント クラスをスキーマタイプとして指定してスキーマを設定します。

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 の詳細については、以下のリソースをご覧ください。

サンプル

フィードバックを送信

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

公開バグトラッカー

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