AppSearch は、ローカルに保存されている構造化データを管理するための、高性能なデバイス上の検索ソリューションです。全文検索を使用してデータをインデックスに登録する API とデータを取得する API を備えています。アプリは AppSearch を使用してカスタムのアプリ内検索機能を提供できるため、ユーザーはオフラインでもコンテンツを検索できます。
AppSearch には次の機能があります。
- I/O 使用率の低い、高速かつモバイル ファーストなストレージの実装
- 大規模なデータセットに対する効率性に優れたインデックス登録とクエリ
- 複数言語のサポート(英語、スペイン語など)
- 関連性ランキングと使用状況スコア
I/O 使用量が少ないため、AppSearch は SQLite と比較して、大規模なデータセットのインデックス登録と検索のレイテンシが低くなります。AppSearch は、単一のクエリをサポートすることで、クロスタイプ クエリを簡素化します。一方、SQLite は、複数のテーブルの結果をマージします。
AppSearch の機能を説明するために、ユーザーのお気に入りの曲を管理し、ユーザーが簡単に検索できるようにする音楽アプリの例を見てみましょう。ユーザーは、さまざまな言語の曲名で世界中の音楽を楽しんでいます。AppSearch は、インデックス登録とクエリをネイティブにサポートしています。ユーザーがタイトルまたはアーティスト名で曲を検索すると、アプリはリクエストを AppSearch に渡すだけで、一致する曲を迅速かつ効率的に取得できます。アプリケーションは結果を表示し、ユーザーはお気に入りの曲をすぐに再生できます。
設定
アプリで AppSearch を使用するには、アプリの build.gradle ファイルに次の依存関係を追加します。
Groovy
dependencies { def appsearch_version = "1.2.0-alpha02" 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 supports sharing data // with other apps. Feature support varies by device. Before calling an API that // requires a feature, check Features.isFeatureSupported(). 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 similar capabilities to 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.2.0-alpha02" 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 supports sharing data // with other apps. Feature support varies by device. Before calling an API that // requires a feature, check Features.isFeatureSupported(). 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 similar capabilities to 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 のコンセプトとその相互作用を示しています。
図 1. AppSearch のコンセプトの図: AppSearch データベース、スキーマ、スキーマタイプ、ドキュメント、セッション、検索。
データベースとセッション
AppSearch データベースは、データベース スキーマに準拠するドキュメントのコレクションです。クライアント アプリケーションは、アプリケーション コンテキストとデータベース名を指定してデータベースを作成します。データベースを開くことができるのは、そのデータベースを作成したアプリケーションのみです。データベースが開かれると、データベースを操作するためのセッションが返されます。セッションは AppSearch API を呼び出すためのエントリ ポイントであり、クライアント アプリケーションによって閉じられるまで開いたままになります。
スキーマとスキーマタイプ
スキーマは、AppSearch データベース内のデータの組織構造を表します。
スキーマは、一意のデータ型を表すスキーマタイプで構成されます。スキーマタイプは、名前、データ型、カーディナリティを含むプロパティで構成されます。スキーマタイプがデータベース スキーマに追加されると、そのスキーマタイプのドキュメントを作成してデータベースに追加できます。
ドキュメント
AppSearch では、データの単位はドキュメントとして表されます。AppSearch データベース内の各ドキュメントは、名前空間と ID によって一意に識別されます。名前空間は、ユーザー アカウントなど、1 つのソースのみをクエリする必要がある場合に、異なるソースのデータを分離するために使用されます。
ドキュメントには、作成タイムスタンプ、有効期間(TTL)、取得時のランキングに使用できるスコアが含まれています。ドキュメントには、ドキュメントに必要な追加のデータ プロパティを記述するスキーマタイプも割り当てられます。
ドキュメント クラスは、ドキュメントの抽象化です。これには、ドキュメントの内容を表すアノテーション付きフィールドが含まれています。デフォルトでは、ドキュメント クラスの名前がスキーマタイプの名前になります。
検索
ドキュメントはインデックスに登録され、クエリを指定して検索できます。ドキュメントにクエリの語句が含まれているか、別の検索仕様に一致する場合、ドキュメントは一致し、検索結果に含まれます。結果は、スコアとランキング戦略に基づいて並べ替えられます。検索結果は、順番に取得できるページで表されます。
AppSearch には、フィルタ、ページサイズ構成、スニペットなど、検索のカスタマイズ機能が用意されています。
プラットフォーム ストレージ、ローカル ストレージ、Play 開発者サービス ストレージ
AppSearch には、LocalStorage、PlatformStorage、PlayServicesStorage の 3 つのストレージ ソリューションが用意されています。LocalStorage では、アプリケーションがアプリケーション データ ディレクトリにあるアプリ固有のインデックスを管理します。PlatformStorage と PlayServicesStorage の両方を使用すると、アプリケーションはシステム全体のセントラル インデックスに貢献します。PlatformStorage のインデックスはシステム サーバーでホストされ、PlayServicesStorage のインデックスは Google Play 開発者サービスのストレージでホストされます。これらの一元化されたインデックス内のデータへのアクセスは、アプリケーションが提供したデータと、別のアプリケーションによって明示的に共有されたデータに制限されます。これらのストレージ オプションはすべて同じ API を共有しており、デバイスのバージョンに基づいて交換できます。
機能のサポートは、ストレージ オプションとデバイスによって異なります。詳細については、機能の提供状況を確認するをご覧ください。
次のコードは、Android 12(API レベル 31)以降では PlatformStorage セッションを作成し、それより前のバージョンでは PlayServicesStorage または LocalStorage セッションを作成します。
Kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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())); } }
PlatformStorage と PlayServicesStorage を使用すると、アプリは他のアプリとデータを安全に共有し、他のアプリがアプリのデータを検索できるようにすることができます。読み取り専用のアプリケーション データ共有は、証明書のハンドシェイクを使用して付与され、他のアプリケーションにデータの読み取り権限があることを確認します。この API の詳細については、setSchemaTypeVisibilityForPackage() のドキュメントをご覧ください。
さらに、PlatformStorage を使用すると、インデックスに登録されたデータをシステム UI サーフェスに表示できます。アプリケーションは、システム UI サーフェスに表示されるデータの一部またはすべてをオプトアウトできます。この API の詳細については、setSchemaTypeDisplayedBySystem() のドキュメントをご覧ください。
| 機能 | LocalStorage(Android 5.0 以降に対応) |
PlatformStorage(Android 12 以降に対応) |
PlayServicesStorage(Android 5.0 以降に対応) |
|---|---|---|---|
| 効率的な全文検索 | |||
| 多言語対応 | |||
| バイナリサイズの縮小 | |||
| アプリケーション間のデータ共有 | |||
| システム UI サーフェスにデータを表示する機能 | |||
| ドキュメントのサイズと数に制限なくインデックスに登録可能 | |||
| バインダのレイテンシを増やすことなく、オペレーションを高速化 |
LocalStorage と PlatformStorage のどちらかを選択する際には、追加のトレードオフを考慮する必要があります。PlatformStorage は AppSearch システム サービスを介して Jetpack API をラップするため、LocalStorage を使用する場合と比較して、APK サイズへの影響は最小限に抑えられます。ただし、これは AppSearch システム サービスを呼び出すときに、AppSearch オペレーションで追加のバインダー レイテンシが発生することも意味します。PlatformStorage を使用すると、AppSearch は、アプリケーションがインデックスに登録できるドキュメントの数とサイズを制限して、効率的な中央インデックスを確保します。PlayServicesStorage には PlatformStorage と同じ制限があり、Google Play 開発者サービスを搭載したデバイスでのみサポートされます。
使用できる機能を確認する
すべてのストレージ オプションで同じ API が共有されますが、一部の機能は、すべてのストレージ オプションまたはすべてのデバイスで使用できるわけではありません。API リファレンスでは、@RequiresFeature アノテーションによって、これらの機能のいずれかを必要とする API が識別されます。Features クラスは、各機能の定数を定義します。たとえば、INDEXING_TYPE_RANGE には Features.NUMERIC_SEARCH が必要です。
PlatformStorage の場合、機能のサポートはデバイスの Android バージョンによって異なります。Android 13(API レベル 33)以降では、機能のサポートはデバイスが受信した Google Play システム アップデートにも依存します。たとえば、PlatformStorage は、次のデバイスでのみ、数値検索(Features.NUMERIC_SEARCH)、逐語検索(Features.VERBATIM_SEARCH)、結合(Features.JOIN_SPEC_AND_QUALIFIED_ID)をサポートしています。
- Android 14(API レベル 34)以降を搭載したデバイス。
- Android 13(API レベル 33)を搭載し、必要な Google Play システム アップデートを受信しているデバイス。
これらの機能は、Android 12 または Android 12L(API レベル 31、32)を搭載したデバイスではサポートされていません。
ドキュメント内のインデックス付きプロパティの最大数も、デバイスとストレージ オプションによって異なります。実行時に上限を取得するには、getMaxIndexedProperties() を呼び出します。
機能のサポートを判断する際に、Android のバージョンだけに依存しないでください。機能を必要とする API を呼び出す前に、AppSearchSession.getFeatures() が返す Features オブジェクトで isFeatureSupported() を呼び出して、その機能がサポートされていることを確認します。必要な機能がサポートされていない場合、オペレーションは例外で失敗します。失敗するケースの例としては、Features.NUMERIC_SEARCH をサポートしていないデバイスで、INDEXING_TYPE_RANGE プロパティを含むスキーマを設定した場合が挙げられます。
次のコードは、開いている AppSearchSession が数値検索をサポートしているかどうかを確認します。
Kotlin
val features = session.features if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) { // Numeric search is supported. You can index properties with // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with // SearchSpec.Builder.setNumericSearchEnabled(true). } else { // Numeric search isn't supported. Set a schema without range-indexed // properties, or switch to a different storage option. }
Java
Features features = session.getFeatures(); if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) { // Numeric search is supported. You can index properties with // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with // SearchSpec.Builder.setNumericSearchEnabled(true). } else { // Numeric search isn't supported. Set a schema without range-indexed // properties, or switch to a different storage option. }
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 という名前の新しいデータベースを作成し、AppSearchSession の ListenableFuture を取得します。これは、データベースへの接続を表し、データベース オペレーション用の 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 の詳細については、以下の参考情報をご覧ください。
サンプル
- Android AppSearch サンプル(Kotlin): AppSearch を使用してユーザーのメモをインデックスに登録し、ユーザーがメモを検索できるようにするメモアプリ。
フィードバックを送信
以下のリソースを通じてフィードバックやアイデアをお寄せください。
Google がバグを修正できるよう問題を報告します。