語意搜尋

本指南將逐步說明如何實作裝置端語意搜尋,這項搜尋技術會根據查詢意義 (而非單純根據關鍵字) 尋找相關內容。語意搜尋可免除手動建構及維護自訂同義詞表格或標記分類的需求。如要在裝置端實作語意搜尋,請使用下列程式庫:

  • ML Kit 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」,然後輕觸「更新」(如有更新)。

支援的模型和設定

ML Kit Embedding 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 個詞元 使用 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 物件,並檢查下列生命週期日期屬性 (以世界標準時間傳回 LocalDate 物件):

  • deprecationDate 值代表模型淘汰期間的開始。您應在模型繼續運作時開始重新建立索引,並在模型正式關閉前完成重新建立索引。
  • shutdownDate 值代表模型停用且不再提供的時間。這個日期過後,模型將無法用於推論,且這個模型的現有嵌入會強制使用最新可用模型重新建立索引。

我們也會透過 ML Kit 版本資訊和開發人員說明文件更新,通知即將淘汰的模型、遷移時程和預定停用日期。

最佳做法

為提供良好的使用者體驗,請在 Android 應用程式中整合向量嵌入時,遵循下列做法:

  • 排定索引工作負載:請一律使用 WorkManager 程式庫排定背景索引工作負載。請設定背景工作,只在裝置充電和閒置時執行。ML Kit 對模型用量設有配額限制,類似於 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 模型的背景資訊,請確認該下游模型及其面向使用者的輸出內容已啟用安全篩選器和內容政策。

其他資源