AppSearch は、ローカルに保存された構造化データを管理するための高性能なオンデバイス検索ソリューションです。全文検索を使用してデータをインデックス登録し、データを取得するための API が含まれています。アプリは 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 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
という名前の新しいデータベースを作成し、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 が修正できるよう、バグを報告します。