本指南将引导您了解如何实现设备端语义搜索,这是一种根据查询含义(而非纯粹根据关键字)查找相关内容的搜索技术。语义搜索无需手动构建和维护自定义同义词表或标记分类。如需在设备端实现语义搜索,请使用以下库:
- 机器学习套件 SDK,特别是 Embedding API,用于生成向量嵌入
- AndroidX AppSearch,用于存储向量嵌入,并使用余弦距离指标搜索结构化数据,以便根据语义查找内容
前提条件
如需使用设备端语义搜索,您必须满足以下设备和账号要求:
- 您的设备支持 Android AICore。 大多数支持 AICore 的设备都使用 NPU 加速模型,而其余受支持的设备则回退到 CPU 模型(使用 XNNPACK 加速)。
- 您设备上的 Google 账号已加入 AICore 开发者预览版计划。
- 您的设备已安装最新版本的 AICore 开发者预览版 APK:在您的设备上,打开 Android AICore Play 商店页面,然后点按更新以安装最新版本的 APK。或者,前往设备设置,搜索系统服务或 Google 服务,点按 Android AICore(Beta 版),然后点按更新(如果有可用更新)。
- 您的设备已安装最新版本的 Private Compute Services 应用:在设备上,打开 Private Compute Services Play 商店页面,然后点按更新以安装最新版本的应用。或者,依次前往 Play 商店 > 管理应用和设备 > 管理 > Private Compute Services,然后点按更新(如果有可用更新)。
支持的模型和配置
机器学习套件嵌入 API 使用 Google 的 EmbeddingGemma 模型系列,该系列由 Android AICore 管理。具体而言,Embedding API 使用 EmbeddingGemma 2 (embedding_gemma_v2) 为文本和图片输入生成嵌入。
以下是输入规范和限制:
| 输入属性 | EmbeddingGemma 2 | 使用说明 |
|---|---|---|
| 支持的模态 | 文字和图片(Modality.TEXT、Modality.IMAGE) |
如需确认是否支持图片输入,请在运行时检查 model.getMetadata()?.supportedModalities.contains(Modality.IMAGE) 是否返回 true。 |
| 词元限制 | 256 个 token | 使用 model.getTokenLimit() 方法进行检查。每张图片会被编码为 70 个 token,剩余 186 个 token 用于随附文本。 |
| 图片分辨率 | 短边不超过 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值表示模型何时停用且不再提供。在此日期之后,该模型将无法用于推理,并且该模型的现有嵌入将被强制使用最新的可用模型重新编入索引。
我们还会通过 机器学习套件版本说明和开发者文档更新,传达即将进行的模型弃用、迁移时间表和目标关停日期。
最佳做法
为了提供良好的用户体验,请在将向量嵌入集成到 Android 应用中时遵循以下做法:
- 安排索引编制工作负载:始终使用 WorkManager 库安排后台索引编制工作负载。将后台任务配置为仅在设备充电和空闲时运行。与 LLM API 类似,机器学习套件对模型用量施加了配额限制,但在设备充电时,限制会更加宽松。在开发者预览版模式下,您可以绕过配额限制进行测试。
- 存储和查询优化:通过使用量化和维度截断来优化数据库占用空间和查询延迟时间:
- 将
AppSearchSchema架构配置为使用QUANTIZATION_TYPE_8_BIT常量,如定义 AppSearch 架构中所述。这会将每个坐标的精度缩减为 8 位整数,从而在对质量影响微乎其微的情况下减少内存用量。 - 使用 EmbeddingGemma 2 模型支持的 Matryoshka 表征学习 (MRL) 截断嵌入向量。如需了解详情,请参阅 HuggingFace 上的 EmbeddingGemma 2。
- 将
- 签名一致性:请勿使用不匹配的签名查询向量空间。如果您升级嵌入模型版本,则必须擦除 AppSearch 数据库,并使用新的模型签名重新生成所有文档的嵌入。
- 安全和内容审核:您负责在应用中实现内容审核和安全过滤。与生成式文本或图片 API 不同,ML Kit Embedding API 不包含内置安全过滤器。由于模型输出的是调用方提供的输入的数值向量表示(
FloatArray坐标),而不是生成新的文本或媒体,因此数值向量本身不会带来生成式内容安全风险。省略设备端安全过滤功能还可避免在高吞吐量文档索引编制和实时查询期间发生 CPU 争用,从而保持低延迟索引编制和搜索性能。为提高安全性,请遵循以下准则:- 管理不可信的输入和已编入索引的内容:如果您的应用会为不可信的用户生成内容或外部数据编制索引或进行搜索,请在为源文本或图片编制索引或显示搜索结果之前,对其应用内容管理和政策过滤条件。
- 保护下游生成式 AI 流水线:如果检索到的文档或段落作为上下文传递给下游生成式 AI 模型,请验证是否在该下游模型及其面向用户的输出中启用了安全过滤器和内容政策。