במדריך הזה מוסבר איך להטמיע חיפוש סמנטי במכשיר, שיטת חיפוש שמוצאת תוכן רלוונטי על סמך המשמעות של השאילתה, ולא רק לפי מילות מפתח. חיפוש סמנטי מייתר את הצורך ליצור ולתחזק באופן ידני טבלאות של מילים נרדפות בהתאמה אישית או טקסונומיות של תיוג. כדי להטמיע חיפוש סמנטי במכשיר, משתמשים בספריות הבאות:
- ML Kit SDK, ובמיוחד Embedding API, כדי ליצור הטמעות של וקטורים
- AndroidX AppSearch לאחסון הטמעות הווקטורים ולחיפוש נתונים מובְנים באמצעות מדדי מרחק קוסינוס כדי למצוא תוכן לפי הסמנטיקה שלו
דרישות מוקדמות
כדי להשתמש בחיפוש סמנטי במכשיר, אתם צריכים לעמוד בדרישות הבאות לגבי המכשיר והחשבון:
- המכשיר שלכם תומך ב-Android AICore. רוב המכשירים שתומכים ב-AICore משתמשים במודלים עם האצת NPU, ובשאר המכשירים הנתמכים נעשה שימוש במודל CPU (עם האצת XNNPACK).
- חשבון Google במכשיר שלכם משתתף בתוכנית תצוגה מקדימה למפתחים של AICore.
- במכשיר מותקנת הגרסה העדכנית של ה-APK של תצוגה מקדימה למפתחים של AICore: פותחים במכשיר את דף חנות Play של Android AICore ולוחצים על עדכון כדי להתקין את הגרסה העדכנית של ה-APK. לחלופין, עוברים אל הגדרות המכשיר, מחפשים את שירותי המערכת או את שירותי Google, לוחצים על Android AICore (בטא) ולוחצים על עדכון אם יש עדכון זמין.
- במכשיר מותקנת הגרסה העדכנית של אפליקציית Private Compute Services: במכשיר, פותחים את הדף של Private Compute Services בחנות Play ולוחצים על עדכון כדי להתקין את הגרסה העדכנית של האפליקציה. לחלופין, עוברים אל חנות Play > ניהול אפליקציות ומכשירים > ניהול > Private Compute Services ולוחצים על עדכון אם יש עדכון זמין.
מודלים והגדרות נתמכים
Embedding API של ML Kit משתמש במשפחת המודלים EmbeddingGemma של Google, שמנוהלת על ידי 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 פיקסלים בצד הקצר יותר | ה-constructor של המחלקה ImagePart מקבל ארגומנטים מסוג Bitmap, Uri או ByteArray. אם המימד הקצר יותר של התמונה גדול מ-768 פיקסלים, ערכת ה-SDK מקטינה את התמונה באופן אוטומטי תוך שמירה על יחס הגובה-רוחב. כדי להשיג ביצועים אופטימליים ושימוש יעיל בזיכרון, כדאי להימנע מיחסי גובה-רוחב קיצוניים. |
המאפיין של וקטור הפלט הוא 768 (אובייקט FloatArray של מספרים ממשיים של 32 ביט). אפשר לבדוק את מאפיין וקטור הפלט בזמן הריצה באמצעות השיטה 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'
// ...
}
כדי לאחזר את המודלים הזמינים, משתמשים ב-method 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 ב-UTC):
- הערך
deprecationDateמציין את תחילת תקופת ההוצאה משימוש של המודל. כדאי להתחיל את ההוספה מחדש לאינדקס בזמן שהמודל ממשיך לפעול, ולסיים את התהליך לפני ההשבתה הרשמית שלו. - הערך של
shutdownDateמציין מתי המודל יושבת ולא יהיה זמין יותר. אחרי התאריך הזה, אי אפשר להשתמש במודל להסקת מסקנות, וצריך להוסיף מחדש לאינדקס הטמעות קיימות של המודל הזה באמצעות המודל הזמין העדכני ביותר.
אנחנו גם מודיעים על הוצאה משימוש של מודלים, על לוחות זמנים להעברה ועל תאריכי יעד להשבתה בנתוני הגרסה של ML Kit ובעדכונים של מאמרי העזרה למפתחים.
שיטות מומלצות
כדי לספק חוויית משתמש טובה, כדאי לפעול לפי השיטות הבאות כשמשלבים הטמעות וקטורים באפליקציה ל-Android:
- תזמון עומסי עבודה של הוספה לאינדקס: תמיד צריך לתזמן עומסי עבודה של הוספה לאינדקס ברקע באמצעות הספרייה WorkManager. מגדירים את משימות הרקע כך שיפעלו רק כשהמכשיר בטעינה ולא פעיל. ב-ML Kit יש מכסות שימוש במודלים, בדומה לממשקי API של LLM, עם מכסות נדיבות יותר כשהמכשיר בטעינה. במצב "תצוגה מקדימה למפתחים" אפשר לעקוף את מגבלות המכסות למטרות בדיקה.
- אופטימיזציה של אחסון ושאילתות: אתם יכולים להשתמש בקוונטיזציה ובחיתוך ממדים כדי לשפר את טביעת הרגל של מסד הנתונים ואת זמן האחזור של השאילתות:
- מגדירים את הסכימה של
AppSearchSchemaכך שתשתמש בקבועQUANTIZATION_TYPE_8_BIT, כמו שמוצג בקטע הגדרת סכימה של AppSearch. כך אפשר להקטין את השימוש בזיכרון עם השפעה מינימלית על האיכות, על ידי הקטנת הדיוק של כל קואורדינטה למספר שלם של 8 ביט. - משתמשים ב-Matryoshka Representation Learning (MRL), שנתמך במודל EmbeddingGemma 2, כדי לקצר את וקטור ההטמעה. מידע נוסף זמין במאמר EmbeddingGemma 2 ב-HuggingFace.
- מגדירים את הסכימה של
- עקביות בחתימה: אל תשלחו שאילתות למרחב וקטורי באמצעות חתימה לא תואמת. אם משדרגים את גרסת המודל להטמעה, צריך למחוק את מסד הנתונים של AppSearch וליצור מחדש הטמעות לכל המסמכים באמצעות החתימה החדשה של המודל.
- בטיחות וניהול תוכן: אתם אחראים להטמיע באפליקציה ניהול תוכן וסינון בטיחות. בניגוד לממשקי API של טקסט דינמי מבוסס-AI או תמונות גנרטיביים, Embedding API של ML Kit לא כולל מסנני בטיחות מובנים. המודל מוציא ייצוגים של וקטורים מספריים (
FloatArrayקואורדינטות) של קלט שסופק על ידי המתקשר, במקום ליצור טקסט או מדיה חדשים. לכן, וקטורים מספריים לא מציגים באופן מהותי סיכוני בטיחות של תוכן גנרטיבי. השמטת סינון הבטיחות במכשיר גם שומרת על ביצועי אינדקס וחיפוש עם זמן אחזור קצר, על ידי מניעת תחרות על משאבי המעבד (CPU) במהלך הוספה לאינדקס של מסמכים עם תפוקה גבוהה ושאילתות בזמן אמת. כדי לשפר את הבטיחות, חשוב לפעול לפי ההנחיות הבאות:- ניהול קלט לא מהימן ותוכן שנוסף לאינדקס: אם האפליקציה מוסיפה לאינדקס או מחפשת תוכן לא מהימן שנוצר על ידי משתמשים או נתונים חיצוניים, צריך להחיל מסנני מדיניות וניהול תוכן על טקסט המקור או התמונות לפני שמוסיפים אותם לאינדקס או מציגים תוצאות חיפוש.
- הגנה על צינורות עיבוד נתונים של AI גנרטיבי במורד הזרם: אם מסמכים או קטעים שאוחזרו מועברים כהקשר למודל AI גנרטיבי במורד הזרם, צריך לוודא שמסנני הבטיחות וכללי המדיניות בנושא תוכן מופעלים במודל הזה במורד הזרם ובפלט שלו שגלוי למשתמשים.