AppSearch は、ローカルに保存されている構造化データを管理するための高パフォーマンスのデバイス上の検索ソリューションです。データのインデックス登録と全文検索によるデータの取得を行う API が含まれています。アプリは AppSearch を使用してカスタムのアプリ内検索機能を提供できます。これにより、ユーザーはオフラインでもコンテンツを検索できます。
![AppSearch 内でのインデックス登録と検索の図](https://developer.android.google.cn/static/images/guide/topics/search/appsearch.png?authuser=6&hl=ja)
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 のコンセプトとそれらの相互作用を示しています。
図 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 を共有し、デバイスのバージョンに応じて相互に交換できます。
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())); } }
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 をラップするため、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
という名前の新しいデータベースを作成し、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 がバグを修正できるよう報告します。