البحث الدلالي

يشرح هذا الدليل كيفية تنفيذ البحث الدلالي على الجهاز فقط، وهو أسلوب بحث يعثر على المحتوى ذي الصلة استنادًا إلى معنى طلب البحث، وليس فقط من خلال الكلمات الرئيسية. يغني البحث الدلالي عن الحاجة إلى إنشاء جداول المرادفات المخصّصة أو تصنيفات وضع العلامات يدويًا وصيانتها. لتنفيذ البحث الدلالي على الجهاز فقط، استخدِم المكتبات التالية:

  • حزمة تطوير البرامج (SDK) الخاصة بـ "حزمة تعلّم الآلة"، وتحديدًا واجهة Embedding API، لإنشاء تضمينات المتّجهات
  • AndroidX AppSearch لتخزين تضمينات المتّجهات والبحث في البيانات المنظَّمة باستخدام مقاييس المسافة بين جيب التمام للعثور على المحتوى حسب دلالاته

المتطلبات الأساسية

لاستخدام ميزة "البحث الدلالي على الجهاز فقط"، يجب استيفاء المتطلبات التالية المتعلقة بالجهاز والحساب:

  • يجب أن يكون جهازك متوافقًا مع Android AICore. تستخدم معظم الأجهزة المتوافقة مع AICore نماذج مسرَّعة بواسطة وحدة المعالجة العصبية (NPU)، بينما تعتمد الأجهزة المتوافقة المتبقية على نموذج وحدة المعالجة المركزية (CPU) (مع تسريع XNNPACK).
  • حساب Google على جهازك هو جزء من برنامج معاينة المطوّر لخدمة AICore.
  • يحتوي جهازك على أحدث إصدار من حزمة APK لمعاينة المطوّرين في AICore: على جهازك، افتح صفحة Android AICore على "متجر Google Play" وانقر على تحديث لتثبيت أحدث إصدار من حزمة APK. بدلاً من ذلك، انتقِل إلى إعدادات جهازك، وابحث عن خدمات النظام أو خدمات Google، وانقر على Android AICore (الإصدار التجريبي)، ثم انقر على تحديث في حال توفّر تحديث.
  • تثبيت أحدث إصدار من تطبيق "خدمات الحوسبة الخاصة" على جهازك: افتح صفحة "خدمات الحوسبة الخاصة" على "متجر Google Play" وانقر على تحديث لتثبيت أحدث إصدار من التطبيق. يمكنك بدلاً من ذلك الانتقال إلى "متجر Google Play" > "إدارة التطبيقات والأجهزة" > "إدارة" > "خدمات الحوسبة الخاصة" والنقر على تحديث في حال توفّر تحديث.

النماذج والإعدادات المتوافقة

تستخدم واجهة Embedding API في "حزمة تعلّم الآلة" مجموعة نماذج 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 بكسل على الجانب الأقصر تقبل الدالة الإنشائية للفئة ImagePart أنواع الوسيطات Bitmap أو Uri أو ByteArray. يتم تلقائيًا تصغير الصور التي يقلّ بُعدها عن 768 بكسل بواسطة حزمة تطوير البرامج (SDK) مع الحفاظ على نسبة العرض إلى الارتفاع. لتحقيق الأداء الأمثل واستخدام الذاكرة بشكل أفضل، تجنَّب نِسب العرض إلى الارتفاع القصوى.

يبلغ بُعد متّجه الناتج 768 (عنصر 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 إلى وقت إيقاف النموذج نهائيًا وعدم إتاحته للاستخدام. وبعد هذا التاريخ، لن يكون النموذج متاحًا للاستنتاج، وستتم إعادة فهرسة التضمينات الحالية لهذا النموذج باستخدام أحدث نموذج متاح.

ونُعلم المطوّرين أيضًا بالنماذج التي سيتم إيقافها نهائيًا، والمخططات الزمنية لعمليات نقل البيانات، وتواريخ الإيقاف النهائية المستهدَفة من خلال ملاحظات إصدار &quot;حزمة تعلّم الآلة&quot; وتعديلات مستندات المطوّرين.

أفضل الممارسات

لتقديم تجربة جيدة للمستخدم، اتّبِع الممارسات التالية عند دمج المتّجهات في تطبيق Android:

  • جدولة أعباء عمل الفهرسة: يجب دائمًا جدولة أعباء عمل الفهرسة في الخلفية باستخدام مكتبة WorkManager. اضبط المهام التي يتم تنفيذها في الخلفية لتعمل فقط عندما يكون الجهاز قيد الشحن وغير مستخدَم من قِبل أي برنامج حاليًا. تفرض حزمة تعلّم الآلة حدودًا على حصة استخدام النماذج، على غرار واجهات برمجة تطبيقات النماذج اللغوية الكبيرة، مع حدود أكثر مرونة عندما يكون الجهاز قيد الشحن. في وضع "معاينة المطوّر"، يمكنك تجاوز حدود الحصة لأغراض الاختبار.
  • تحسين مساحة التخزين وطلبات البحث: يمكنك تحسين حجم قاعدة البيانات ووقت استجابة طلبات البحث باستخدام ميزتَي "التقدير الكمّي" و"اقتطاع الأبعاد":
    • اضبط مخطط AppSearchSchema لاستخدام الثابت QUANTIZATION_TYPE_8_BIT، كما هو موضّح في تحديد مخطط AppSearch. يؤدي ذلك إلى تقليل استخدام الذاكرة مع الحد الأدنى من التأثير في الجودة من خلال تقليص دقة كل إحداثي إلى عدد صحيح مكوّن من 8 بت.
    • استخدِم تقنية Matryoshka Representation Learning (MRL) التي يتيحها نموذج EmbeddingGemma 2 لاقتطاع متّجه التضمين. لمزيد من المعلومات، يُرجى الاطّلاع على EmbeddingGemma 2 على HuggingFace.
  • اتساق التوقيع: لا تستعلم عن مساحة متجه باستخدام توقيع غير متطابق. في حال ترقية إصدار نموذج التضمين، يجب محو قاعدة بيانات AppSearch وإعادة إنشاء التضمينات لجميع المستندات باستخدام توقيع النموذج الجديد.
  • الأمان والإشراف على المحتوى: أنت مسؤول عن تنفيذ الإشراف على المحتوى وفلترة الأمان في تطبيقك. على عكس واجهات برمجة التطبيقات الخاصة بالنصوص أو الصور من إنشاء الذكاء الاصطناعي التوليدي، لا تتضمّن واجهة Embedding API في "حزمة تعلّم الآلة" فلاتر أمان مدمجة. بما أنّ النموذج يُخرج تمثيلات متجهات رقمية (إحداثيات FloatArray) للمدخلات التي يقدّمها المتصل بدلاً من إنشاء نصوص أو وسائط جديدة، لا تشكّل المتجهات الرقمية بطبيعتها مخاطر على أمان المحتوى التوليدي. يؤدي حذف فلترة الأمان على الجهاز فقط أيضًا إلى الحفاظ على أداء الفهرسة والبحث بزمن انتقال منخفض من خلال تجنُّب تنافس وحدة المعالجة المركزية (CPU) أثناء فهرسة المستندات بمعدّل نقل بيانات مرتفع وطلبات البحث في الوقت الفعلي. لتحسين مستوى الأمان، اتّبِع الإرشادات التالية:
    • الإشراف على المدخلات غير الموثوق بها والمحتوى المفهرَس: إذا كان تطبيقك يفهرس أو يبحث في محتوى غير موثوق به من إنشاء المستخدمين أو بيانات خارجية، يجب تطبيق فلاتر الإشراف على المحتوى والسياسات على النصوص أو الصور المصدر قبل فهرستها أو عرض نتائج البحث.
    • حماية مسارات الذكاء الاصطناعي التوليدي اللاحقة: إذا تم تمرير المستندات أو الفقرات التي تم استردادها كسياق إلى نموذج ذكاء اصطناعي توليدي لاحق، يجب التأكّد من تفعيل فلاتر الأمان وسياسات المحتوى في هذا النموذج اللاحق ومخرجاته الموجّهة للمستخدمين.

مراجع إضافية