このガイドでは、オンデバイスのセマンティック検索を実装する方法について説明します。セマンティック検索は、キーワードだけでなく、クエリの意味に基づいて関連性の高いコンテンツを見つける検索手法です。セマンティック検索を使用すると、カスタムの同義語テーブルやタグ付け分類を手動で作成して維持する必要がなくなります。デバイスでセマンティック検索を実装するには、次のライブラリを使用します。
- ML Kit SDK(特に Embedding API)を使用して、ベクトル エンベディングを生成する
- AndroidX AppSearch: ベクトル エンベディングを保存し、コサイン距離指標を使用して構造化データを検索し、セマンティクスに基づいてコンテンツを検索します。
前提条件
オンデバイス セマンティック検索を使用するには、次のデバイスとアカウントの要件を満たす必要があります。
- デバイスが Android AICore をサポートしている。AICore 対応デバイスのほとんどは NPU アクセラレーション モデルを使用しますが、残りの対応デバイスは CPU モデル(XNNPACK アクセラレーションを使用)にフォールバックします。
- デバイスの Google アカウントが AICore デベロッパー プレビュー プログラムに登録されている。
- デバイスに最新バージョンの AICore デベロッパー プレビュー APK がインストールされている。デバイスで Android AICore の Google Play ストア ページを開き、[更新] をタップして最新バージョンの APK をインストールします。または、デバイスの [設定] に移動し、[システム サービス] または [Google サービス] を検索して、[Android AICore(ベータ版)] をタップし、アップデートが利用可能な場合は [アップデート] をタップします。
- デバイスに最新バージョンの Private Compute Services アプリがインストールされている。デバイスで Private Compute Services の Google Play ストア ページを開き、[更新] をタップして最新バージョンのアプリをインストールします。または、[Google Play ストア] > [アプリとデバイスの管理] > [管理] > [Private Compute Services] に移動し、アップデートが利用可能な場合は [更新] をタップします。
サポートされているモデルと構成
ML Kit Embedding API は、Android AICore によって管理される Google の EmbeddingGemma モデル ファミリーを使用します。具体的には、Embedding API は EmbeddingGemma 2(embedding_gemma_v2)を使用して、テキスト入力と画像入力のエンベディングを生成します。
入力の仕様と制限は次のとおりです。
| 入力プロパティ | EmbeddingGemma 2 | 使用上の注意 |
|---|---|---|
| サポートされているモダリティ | テキストと画像(Modality.TEXT、Modality.IMAGE) |
画像入力がサポートされていることを確認するには、実行時に model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) が true を返すことを確認します。 |
| トークンの上限 | 256 トークン | model.getTokenLimit() メソッドを使用して検査します。各画像は 70 トークンにエンコードされ、付随するテキストに 186 トークンが残ります。 |
| 画像解像度 | 短い辺が 768 ピクセル以下 | ImagePart クラスのコンストラクタは、Bitmap、Uri、ByteArray の引数タイプを受け入れます。短辺が 768 ピクセルを超える画像は、アスペクト比を維持したまま SDK によって自動的に縮小されます。パフォーマンスとメモリ使用量を最適化するには、極端なアスペクト比を避けてください。 |
出力ベクトル ディメンションは 768(32 ビット浮動小数点の FloatArray オブジェクト)です。出力ベクトル ディメンションは、実行時に model.getMetadata()?.dimension メソッドを使用して検査できます。
始める
セマンティック検索を使用するようにプロジェクトを構成するには、モジュール レベルの build.gradle.kts(または build.gradle)ファイルに次の依存関係を追加します。
Kotlin
dependencies {
// ...
implementation("com.google.mlkit:genai-embedding:1.0.0-alpha1")
implementation("androidx.appsearch:appsearch:1.2.0-alpha01")
implementation("androidx.appsearch:appsearch-local-storage:1.2.0-alpha01")
implementation("androidx.concurrent:concurrent-futures-ktx:1.3.0")
// ...
}
Groovy
dependencies {
// ...
implementation 'com.google.mlkit:genai-embedding:1.0.0-alpha1'
implementation 'androidx.appsearch:appsearch:1.2.0-alpha01'
implementation 'androidx.appsearch:appsearch-local-storage:1.2.0-alpha01'
implementation 'androidx.concurrent:concurrent-futures-ktx:1.3.0'
// ...
}
使用可能なモデルを取得するには、EmbeddingGeneration.getEmbeddingModels(EmbeddingOptions()) メソッドを使用します。
AppSearch スキーマを定義する
セマンティック類似性検索を有効にするには、類似性インデックス作成用にベクトル プロパティが明示的に構成されている AppSearch スキーマを定義する必要があります。
// Define the document schema
val documentSchema = AppSearchSchema.Builder("my_schema_type")
.addProperty(
AppSearchSchema.StringPropertyConfig.Builder("text")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_PLAIN)
.build()
)
.addProperty(
AppSearchSchema.StringPropertyConfig.Builder("imageUri")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_NONE)
.setTokenizerType(AppSearchSchema.StringPropertyConfig.TOKENIZER_TYPE_NONE)
.build()
)
.addProperty(
AppSearchSchema.EmbeddingPropertyConfig.Builder("embedding")
.setCardinality(AppSearchSchema.PropertyConfig.CARDINALITY_OPTIONAL)
.setIndexingType(
AppSearchSchema.EmbeddingPropertyConfig.INDEXING_TYPE_SIMILARITY)
.setQuantizationType(
AppSearchSchema.EmbeddingPropertyConfig.QUANTIZATION_TYPE_8_BIT)
.build()
)
.build()
ドキュメント コンテンツのインデックスを登録する
ドキュメント コンテンツをインデックスに登録するには、各 Content オブジェクトのベクトル エンベディングを生成し、GenericDocument インスタンスにラップして、AppSearch データベースに保存する必要があります。
EmbeddingVector オブジェクトを構築するときは、アクティブな modelSignature 値も渡す必要があります。AppSearch は、この modelSignature 文字列を使用して、互換性のない検索クエリを防止します。
suspend fun syncDatabase(documents: List<Pair<String, Uri?>>) {
val modelSignature = model?.getModelSignature() ?: return
val docsWithEmbedding = mutableListOf<GenericDocument>()
for ((index, item) in documents.withIndex()) {
val (content, imageUri) = item
// 1. Generate embedding (text and/or image)
val parts = mutableListOf<Part>()
if (content.isNotBlank()) {
parts.add(TextPart(content))
}
var imagePart: ImagePart? = null
val vector = try {
if (imageUri != null && isImageSupported) {
imagePart = ImagePart(imageUri)
parts.add(imagePart)
}
if (parts.isEmpty()) continue
val request = EmbeddingRequest(
Content(parts)
)
model?.generateEmbedding(request)?.embedding?.vector
?: continue
} finally {
imagePart?.close()
}
// 2. Build the AppSearch document
val docBuilder = GenericDocument.Builder<GenericDocument.Builder<*>>(
"my_namespace",
"doc_$index",
"my_schema_type"
)
.setPropertyString("text", content)
// Associate the vector with the signature that generated it
.setPropertyEmbedding("embedding", EmbeddingVector(vector, modelSignature))
if (imageUri != null) {
docBuilder.setPropertyString("imageUri", imageUri.toString())
}
docsWithEmbedding.add(docBuilder.build())
}
// 3. Clear the old database by force-overriding the schema
val clearRequest = SetSchemaRequest.Builder().setForceOverride(true).build()
session?.setSchemaAsync(clearRequest)?.await()
val schemaRequest = SetSchemaRequest.Builder().addSchemas(documentSchema).build()
session?.setSchemaAsync(schemaRequest)?.await()
// 4. Index documents
val putRequest = PutDocumentsRequest.Builder().addGenericDocuments(docsWithEmbedding).build()
session?.putAsync(putRequest)?.await()
}
セマンティック検索を実行する
セマンティック クエリを実行するには、検索クエリ(テキストや画像)をエンベディング ベクトルに変換し、ベクトル検索パラメータを構成して、AppSearch データベースにクエリを実行します。
推奨:
- デフォルトの指標を
SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE定数に設定します。 "sum(this.matchedSemanticScores(getEmbeddingParameter(0)))"数式で結果をランク付けします。- 論理
"semanticSearch(getEmbeddingParameter(0), $threshold)"文字列を使用して、類似度がしきい値(通常は 0.5 以上)を満たす結果をフィルタします。
suspend fun search(
queryText: String,
imageUri: Uri? = null,
threshold: Double = 0.5
): List<Triple<String, Uri?, Double>> {
val modelSignature = model?.getModelSignature() ?: return emptyList()
// 1. Generate query embedding (text and/or image)
val parts = mutableListOf<Part>()
if (queryText.isNotBlank()) {
parts.add(TextPart(queryText))
}
var imagePart: ImagePart? = null
val queryVector = try {
if (imageUri != null && isImageSupported) {
imagePart = ImagePart(imageUri)
parts.add(imagePart)
}
if (parts.isEmpty()) return emptyList()
val request = EmbeddingRequest(
content = Content(parts)
)
model?.generateEmbedding(request)?.embedding?.vector
?: return emptyList()
} finally {
imagePart?.close()
}
// 2. Configure search specifications
val searchSpec = SearchSpec.Builder()
.addFilterSchemas("my_schema_type")
.setDefaultEmbeddingSearchMetricType(SearchSpec.EMBEDDING_SEARCH_METRIC_TYPE_COSINE)
.setRankingStrategy("sum(this.matchedSemanticScores(getEmbeddingParameter(0)))")
.setListFilterQueryLanguageEnabled(true)
.setResultCountPerPage(5)
// Add the query vector as parameter 0
.addEmbeddingParameters(EmbeddingVector(queryVector, modelSignature))
.build()
// 3. Execute search
val queryExpression = "semanticSearch(getEmbeddingParameter(0), $threshold)"
val results = session?.search(queryExpression, searchSpec) ?: return emptyList()
return try {
val page = results.nextPageAsync.await()
page.map {
val text = it.genericDocument.getPropertyString("text") ?: ""
val matchedImageUri = it.genericDocument.getPropertyString("imageUri")?.let(Uri::parse)
// rankingSignal contains the computed similarity score
Triple(text, matchedImageUri, it.rankingSignal)
}
} finally {
results.close()
}
}
ハイブリッド検索インデックスを使用する
優れたユーザー エクスペリエンスを実現するには、セマンティック検索とキーワード検索の両方を含むハイブリッド戦略を使用することをおすすめします。ユーザーが検索バーに入力しているときは、キーワード検索を使用して、即座に予測入力候補を表示します。ユーザーがクエリの入力を完了したとき、または単語の区切りでも、セマンティック検索を使用して、最適な概念の一致を読み取り、理解し、取得します。
ベクトル データベースのインデックスを再作成する
異なるモデル バージョンで生成されたエンベディングは、互換性のないベクトル空間に存在するため、直接比較することはできません。エンベディング モデルを更新するたびに、AppSearch データベースを再インデックスする必要があります。
使用しているモデルが非推奨期間に入ったら、すぐにエンベディングの再インデックスを開始することをおすすめします。Embedding API を使用して、モデルのサポート終了スケジュールを確認できます。model.getMetadata() メソッドを呼び出して EmbeddingMetadata オブジェクトを取得し、次のライフサイクル日付プロパティ(UTC の LocalDate オブジェクトとして返されます)を確認します。
deprecationDateの値は、モデルのサポート終了期間の開始を示します。モデルが引き続き動作している間に再インデックス登録を開始し、正式なシャットダウンの前に再インデックス登録を完了する必要があります。shutdownDateの値は、モデルが廃止され、提供されなくなるタイミングです。この日以降、このモデルは推論に使用できなくなり、このモデルの既存のエンベディングは、利用可能な最新のモデルで強制的に再インデックス化されます。
また、今後のモデルのサポート終了、移行スケジュール、廃止予定日については、ML Kit のリリースノートとデベロッパー向けドキュメントの更新でもお知らせします。
ベスト プラクティス
優れたユーザー エクスペリエンスを提供するため、ベクトル エンベディングを Android アプリに統合する際は、次のプラクティスに沿ってください。
- インデックス登録ワークロードをスケジュール設定する: 常に WorkManager ライブラリを使用して、バックグラウンド インデックス登録ワークロードをスケジュール設定します。デバイスが充電中でアイドル状態のときにのみ実行されるようにバックグラウンド タスクを構成します。ML Kit では、LLM API と同様に、モデルの使用量に割り当て制限が適用されます。デバイスの充電中は、より緩やかな制限が適用されます。デベロッパー プレビュー モードでは、テスト目的で割り当て制限をバイパスできます。
- ストレージとクエリの最適化: 量子化と次元の切り捨てを使用して、データベースのフットプリントとクエリのレイテンシを最適化します。
- AppSearch スキーマを定義するに示されているように、
QUANTIZATION_TYPE_8_BIT定数を使用するようにAppSearchSchemaスキーマを構成します。これにより、各座標の精度を 8 ビット整数に縮小することで、品質への影響を最小限に抑えながらメモリ使用量を削減します。 - EmbeddingGemma 2 モデルでサポートされているマトリョーシカ特徴表現学習(MRL)を使用して、エンベディング ベクトルを切り捨てます。詳細については、HuggingFace の EmbeddingGemma 2 をご覧ください。
- AppSearch スキーマを定義するに示されているように、
- 署名の整合性: 一致しない署名を使用してベクトル空間をクエリしないでください。エンベディング モデルのバージョンをアップグレードする場合は、AppSearch データベースをワイプし、新しいモデル署名を使用してすべてのドキュメントのエンベディングを再生成する必要があります。
- 安全性とコンテンツ管理: アプリにコンテンツ管理と安全性フィルタを実装する責任は、お客様にあります。生成テキスト API や画像 API とは異なり、ML Kit Embedding API には組み込みの安全性フィルタは含まれていません。このモデルは、新しいテキストやメディアを生成するのではなく、呼び出し元が指定した入力の数値ベクトル表現(
FloatArray座標)を出力するため、数値ベクトルには生成コンテンツの安全性リスクが本質的に存在しません。オンデバイスの安全性フィルタリングを省略すると、高スループットのドキュメント インデックス登録やリアルタイム クエリ中の CPU 競合を回避できるため、低レイテンシのインデックス登録と検索パフォーマンスを維持できます。安全性を高めるには、次のガイドラインに沿って対応してください。- 信頼できない入力とインデックスに登録されたコンテンツを管理する: アプリが信頼できないユーザー作成コンテンツや外部データをインデックスに登録または検索する場合は、インデックスに登録する前、または検索結果を表示する前に、ソーステキストまたは画像にコンテンツ管理とポリシー フィルタを適用します。
- ダウンストリームの生成 AI パイプラインを保護する: 取得したドキュメントまたはパッセージがコンテキストとしてダウンストリームの生成 AI モデルに渡される場合は、そのダウンストリーム モデルとそのユーザー向け出力で安全フィルタとコンテンツ ポリシーが有効になっていることを確認します。