البحث في التطبيقات

AppSearch هو حلّ عالي الأداء للبحث على الجهاز فقط لإدارة البيانات المنظَّمة والمخزنة محليًا. تحتوي على واجهات برمجة تطبيقات لفهرسة البيانات واسترداد البيانات باستخدام البحث في النص الكامل. يمكن للتطبيقات استخدام AppSearch لتقديم إمكانات بحث مخصّصة داخل التطبيق، ما يتيح للمستخدمين البحث عن المحتوى حتى بدون اتصال بالإنترنت.

رسم بياني يوضّح الفهرسة والبحث ضمن AppSearch

توفّر AppSearch الميزات التالية:

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

بسبب انخفاض نسبة استخدام وحدات الإدخال والإخراج، توفّر خدمة AppSearch وقت استجابة أقل للفهرسة والبحث في مجموعات البيانات الكبيرة مقارنةً بخدمة SQLite. تعمل AppSearch على تبسيط طلبات البحث متعدّدة الأنواع من خلال إتاحة طلبات بحث فردية، بينما تدمج SQLite النتائج من جداول متعددة.

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

ضبط إعدادات

لاستخدام AppSearch في تطبيقك، أضِف التبعيات التالية إلى ملف build.gradle الخاص بتطبيقك:

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha03"

    implementation "androidx.appsearch:appsearch:$appsearch_version"
    // Use kapt instead of annotationProcessor if writing Kotlin classes
    annotationProcessor "androidx.appsearch:appsearch-compiler:$appsearch_version"

    implementation "androidx.appsearch:appsearch-local-storage:$appsearch_version"
    // PlatformStorage is compatible with Android 12+ devices, and offers additional features
    // to LocalStorage.
    implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version"
}

Kotlin

dependencies {
    val appsearch_version = "1.1.0-alpha03"

    implementation("androidx.appsearch:appsearch:$appsearch_version")
    // Use annotationProcessor instead of kapt if writing Java classes
    kapt("androidx.appsearch:appsearch-compiler:$appsearch_version")

    implementation("androidx.appsearch:appsearch-local-storage:$appsearch_version")
    // PlatformStorage is compatible with Android 12+ devices, and offers additional features
    // to LocalStorage.
    implementation("androidx.appsearch:appsearch-platform-storage:$appsearch_version")
}

مفاهيم AppSearch

يوضِّح الرسم البياني التالي مفاهيم AppSearch وتفاعلاتها.

رسم تخطيطي لتطبيق عميل والتفاعلات معه مع مفاهيم AppSearch التالية: قاعدة بيانات AppSearch والمخطط وأنواع المخططات والمستندات والجلسة والبحث الشكل 1. رسم توضيحي لمفاهيم AppSearch: قاعدة بيانات AppSearch والمخطط وأنواع المخطّطات والمستندات والجلسات والبحث

قاعدة البيانات والجلسة

قاعدة بيانات AppSearch هي مجموعة من المستندات التي تتوافق مع مخطط قاعدة البيانات. تنشئ تطبيقات العميل قاعدة بيانات من خلال توفير سياق التطبيق الخاص بها واسم قاعدة البيانات. لا يمكن فتح قواعد البيانات إلا من خلال التطبيق الذي أنشأها. عند فتح قاعدة بيانات، يتم إرجاع جلسة للتفاعل مع قاعدة البيانات. هذه الجلسة هي نقطة الدخول لطلب واجهات برمجة تطبيقات AppSearch وتظل مفتوحة إلى أن يغلقها تطبيق العميل.

أنواع المخططات والمخططات

يمثل المخطط البنية التنظيمية للبيانات داخل قاعدة بيانات AppSearch.

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

المستندات

في AppSearch، يتم تمثيل وحدة بيانات كمستند. يتم تحديد كل مستند في قاعدة بيانات AppSearch بشكلٍ فريد من خلال مساحة الاسم ورقم التعريف الخاصَين به. يتم استخدام مساحات الأسماء لفصل البيانات من مصادر مختلفة عندما يلزم الاستعلام عن مصدر واحد فقط، مثل حسابات المستخدمين.

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

فئة المستند هي فكرة مجرّدة عن مستند. حيث يحتوي على حقول ذات تعليقات توضيحية تمثل محتويات المستند. بشكل افتراضي، يحدد اسم فئة المستند اسم نوع المخطط.

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

يوفر AppSearch تخصيصات للبحث، مثل الفلاتر وإعدادات حجم الصفحة والمقتطفات.

مقارنة بين مساحة التخزين على النظام الأساسي ومساحة التخزين المحلية

تقدم AppSearch حلَّين للتخزين: LocalStorage وplatformStorage. باستخدام LocalStorage، يدير تطبيقك فهرسًا خاصًا بالتطبيق يكون متوفرًا في دليل بيانات التطبيق. باستخدام PlatformStorage، يساهم التطبيق الخاص بك في فهرس مركزي على مستوى النظام. يقتصر الوصول إلى البيانات داخل الفهرس المركزي على البيانات التي ساهم بها تطبيقك والبيانات التي تمت مشاركتها معك صراحةً من خلال تطبيق آخر. يتشارك كل من LocalStorage وPlatformStorage واجهة برمجة التطبيقات نفسها ويمكن التبديل بينهما بناءً على إصدار الجهاز:

Kotlin

if (BuildCompat.isAtLeastS()) {
    appSearchSessionFuture.setFuture(
        PlatformStorage.createSearchSession(
            PlatformStorage.SearchContext.Builder(mContext, DATABASE_NAME)
               .build()
        )
    )
} else {
    appSearchSessionFuture.setFuture(
        LocalStorage.createSearchSession(
            LocalStorage.SearchContext.Builder(mContext, DATABASE_NAME)
                .build()
        )
    )
}

Java

if (BuildCompat.isAtLeastS()) {
    mAppSearchSessionFuture.setFuture(PlatformStorage.createSearchSession(
            new PlatformStorage.SearchContext.Builder(mContext, DATABASE_NAME)
                    .build()));
} else {
    mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
            new LocalStorage.SearchContext.Builder(mContext, DATABASE_NAME)
                    .build()));
}

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

بالإضافة إلى ذلك، يمكن عرض البيانات المفهرَسة على مساحات عرض واجهة مستخدم النظام. يمكن للتطبيقات إيقاف عرض بعض أو كل البيانات على مساحات عرض واجهة مستخدم النظام. اطّلع على المزيد من المعلومات عن واجهة برمجة التطبيقات هذه في وثائق setSchemaTypeDisplayedBySystem().

الميزات LocalStorage (compatible with Android 4.0+) PlatformStorage (compatible with Android 12+)
Efficient full-text search
Multi-language support
Reduced binary size
Application-to-application data sharing
Capability to display data on System UI surfaces
Unlimited document size and count can be indexed
Faster operations without additional binder latency

هناك حلول إضافية يجب أخذها في الاعتبار عند الاختيار بين LocalStorage وplatformStorage. نظرًا لأن PlatformStorage يغطّي واجهات برمجة تطبيقات Jetpack على خدمة النظام AppSearch، فإنّ تأثير حجم حزمة APK يكون ضئيلاً مقارنةً باستخدام LocalStorage. ومع ذلك، يعني هذا أيضًا أنّ عمليات AppSearch تتأثّر بوقت استجابة إضافي للملف عند استدعاء خدمة نظام AppSearch. باستخدام PlatformStorage، يحد AppSearch من عدد المستندات وحجم المستندات التي يمكن للتطبيق فهرستها لضمان فهرس مركزي فعال.

بدء استخدام AppSearch

يعرض المثال في هذا القسم كيفية استخدام واجهات برمجة تطبيقات AppSearch للتكامل مع تطبيق افتراضي للاحتفاظ بالملاحظات.

كتابة فئة المستند

تتمثل الخطوة الأولى للتكامل مع AppSearch في كتابة فئة مستند لوصف البيانات المراد إدراجها في قاعدة البيانات. يمكنك وضع علامة على فئة كفئة المستند باستخدام التعليق التوضيحي @Document.ويمكنك استخدام مثيلات فئة المستند لوضع المستندات في قاعدة البيانات واستردادها.

يحدّد الرمز التالي فئة مستند ملاحظة باستخدام حقل @Document.StringProperty يتضمّن تعليقات توضيحية لفهرسة نص عنصر ملاحظة.

Kotlin

@Document
public data class Note(

    // Required field for a document class. All documents MUST have a namespace.
    @Document.Namespace
    val namespace: String,

    // Required field for a document class. All documents MUST have an Id.
    @Document.Id
    val id: String,

    // Optional field for a document class, used to set the score of the
    // document. If this is not included in a document class, the score is set
    // to a default of 0.
    @Document.Score
    val score: Int,

    // Optional field for a document class, used to index a note's text for this
    // document class.
    @Document.StringProperty(indexingType = AppSearchSchema.StringPropertyConfig.INDEXING_TYPE_PREFIXES)
    val text: String
)

Java

@Document
public class Note {

  // Required field for a document class. All documents MUST have a namespace.
  @Document.Namespace
  private final String namespace;

  // Required field for a document class. All documents MUST have an Id.
  @Document.Id
  private final String id;

  // Optional field for a document class, used to set the score of the
  // document. If this is not included in a document class, the score is set
  // to a default of 0.
  @Document.Score
  private final int score;

  // Optional field for a document class, used to index a note's text for this
  // document class.
  @Document.StringProperty(indexingType = StringPropertyConfig.INDEXING_TYPE_PREFIXES)
  private final String text;

  Note(@NonNull String id, @NonNull String namespace, int score, @NonNull String text) {
    this.id = Objects.requireNonNull(id);
    this.namespace = Objects.requireNonNull(namespace);
    this.score = score;
    this.text = Objects.requireNonNull(text);
  }

  @NonNull
  public String getNamespace() {
    return namespace;
  }

  @NonNull
  public String getId() {
    return id;
  }

  public int getScore() {
    return score;
  }

  @NonNull
  public String getText() {
     return text;
  }
}

فتح قاعدة بيانات

يجب عليك إنشاء قاعدة بيانات قبل العمل مع المستندات. ينشئ الرمز التالي قاعدة بيانات جديدة باسم notes_app ويحصل على ListenableFuture لـ AppSearchSession، والتي تمثل الاتصال بقاعدة البيانات وتوفِّر واجهات برمجة التطبيقات لعمليات قاعدة البيانات.

Kotlin

val context: Context = getApplicationContext()
val sessionFuture = LocalStorage.createSearchSession(
    LocalStorage.SearchContext.Builder(context, /*databaseName=*/"notes_app")
    .build()
)

Java

Context context = getApplicationContext();
ListenableFuture<AppSearchSession> sessionFuture = LocalStorage.createSearchSession(
       new LocalStorage.SearchContext.Builder(context, /*databaseName=*/ "notes_app")
               .build()
);

وضع مخطط

يجب عليك تعيين مخطط قبل وضع المستندات في قاعدة البيانات واستردادها. يتكون مخطط قاعدة البيانات من أنواع مختلفة من البيانات المهيكلة يشار إليها باسم "أنواع المخططات". تحدد التعليمة البرمجية التالية المخطط من خلال توفير فئة المستند كنوع مخطط.

Kotlin

val setSchemaRequest = SetSchemaRequest.Builder().addDocumentClasses(Note::class.java)
    .build()
val setSchemaFuture = Futures.transformAsync(
    sessionFuture,
    { session ->
        session?.setSchema(setSchemaRequest)
    }, mExecutor
)

Java

SetSchemaRequest setSchemaRequest = new SetSchemaRequest.Builder().addDocumentClasses(Note.class)
       .build();
ListenableFuture<SetSchemaResponse> setSchemaFuture =
       Futures.transformAsync(sessionFuture, session -> session.setSchema(setSchemaRequest), mExecutor);

وضع مستند في قاعدة البيانات

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

Kotlin

val note = Note(
    namespace="user1",
    id="noteId",
    score=10,
    text="Buy fresh fruit"
)

val putRequest = PutDocumentsRequest.Builder().addDocuments(note).build()
val putFuture = Futures.transformAsync(
    sessionFuture,
    { session ->
        session?.put(putRequest)
    }, mExecutor
)

Futures.addCallback(
    putFuture,
    object : FutureCallback<AppSearchBatchResult<String, Void>?> {
        override fun onSuccess(result: AppSearchBatchResult<String, Void>?) {

            // Gets map of successful results from Id to Void
            val successfulResults = result?.successes

            // Gets map of failed results from Id to AppSearchResult
            val failedResults = result?.failures
        }

        override fun onFailure(t: Throwable) {
            Log.e(TAG, "Failed to put documents.", t)
        }
    },
    mExecutor
)

Java

Note note = new Note(/*namespace=*/"user1", /*id=*/
                "noteId", /*score=*/ 10, /*text=*/ "Buy fresh fruit!");

PutDocumentsRequest putRequest = new PutDocumentsRequest.Builder().addDocuments(note)
       .build();
ListenableFuture<AppSearchBatchResult<String, Void>> putFuture =
       Futures.transformAsync(sessionFuture, session -> session.put(putRequest), mExecutor);

Futures.addCallback(putFuture, new FutureCallback<AppSearchBatchResult<String, Void>>() {
   @Override
   public void onSuccess(@Nullable AppSearchBatchResult<String, Void> result) {

     // Gets map of successful results from Id to Void
     Map<String, Void> successfulResults = result.getSuccesses();

     // Gets map of failed results from Id to AppSearchResult
     Map<String, AppSearchResult<Void>> failedResults = result.getFailures();
   }

   @Override
   public void onFailure(@NonNull Throwable t) {
      Log.e(TAG, "Failed to put documents.", t);
   }
}, mExecutor);

يمكنك البحث في المستندات التي تمت فهرستها باستخدام عمليات البحث المشمولة في هذا القسم. تُجري التعليمة البرمجية التالية طلبات عن مصطلح "fruit" على قاعدة البيانات للمستندات التي تنتمي إلى مساحة الاسم user1.

Kotlin

val searchSpec = SearchSpec.Builder()
    .addFilterNamespaces("user1")
    .build();

val searchFuture = Futures.transform(
    sessionFuture,
    { session ->
        session?.search("fruit", searchSpec)
    },
    mExecutor
)
Futures.addCallback(
    searchFuture,
    object : FutureCallback<SearchResults> {
        override fun onSuccess(searchResults: SearchResults?) {
            iterateSearchResults(searchResults)
        }

        override fun onFailure(t: Throwable?) {
            Log.e("TAG", "Failed to search notes in AppSearch.", t)
        }
    },
    mExecutor
)

Java

SearchSpec searchSpec = new SearchSpec.Builder()
       .addFilterNamespaces("user1")
       .build();

ListenableFuture<SearchResults> searchFuture =
       Futures.transform(sessionFuture, session -> session.search("fruit", searchSpec),
       mExecutor);

Futures.addCallback(searchFuture,
       new FutureCallback<SearchResults>() {
           @Override
           public void onSuccess(@Nullable SearchResults searchResults) {
               iterateSearchResults(searchResults);
           }

           @Override
           public void onFailure(@NonNull Throwable t) {
               Log.e(TAG, "Failed to search notes in AppSearch.", t);
           }
       }, mExecutor);

التكرار عبر نتائج البحث

تعرض عمليات البحث مثيل SearchResults، الذي يتيح الوصول إلى صفحات عناصر SearchResult. تحتفظ كل SearchResult بقيمة GenericDocument المطابقة، وهي النموذج العام لمستند يتم تحويل جميع المستندات إليه. يحصل الرمز التالي على الصفحة الأولى من نتائج البحث ويحوّل النتيجة إلى مستند Note مرة أخرى.

Kotlin

Futures.transform(
    searchResults?.nextPage,
    { page: List<SearchResult>? ->
        // Gets GenericDocument from SearchResult.
        val genericDocument: GenericDocument = page!![0].genericDocument
        val schemaType = genericDocument.schemaType
        val note: Note? = try {
            if (schemaType == "Note") {
                // Converts GenericDocument object to Note object.
                genericDocument.toDocumentClass(Note::class.java)
            } else null
        } catch (e: AppSearchException) {
            Log.e(
                TAG,
                "Failed to convert GenericDocument to Note",
                e
            )
            null
        }
        note
    },
    mExecutor
)

Java

Futures.transform(searchResults.getNextPage(), page -> {
  // Gets GenericDocument from SearchResult.
  GenericDocument genericDocument = page.get(0).getGenericDocument();
  String schemaType = genericDocument.getSchemaType();

  Note note = null;

  if (schemaType.equals("Note")) {
    try {
      // Converts GenericDocument object to Note object.
      note = genericDocument.toDocumentClass(Note.class);
    } catch (AppSearchException e) {
      Log.e(TAG, "Failed to convert GenericDocument to Note", e);
    }
  }

  return note;
}, mExecutor);

إزالة مستند

عندما يحذف المستخدم ملاحظة، يحذف التطبيق مستند Note المقابل من قاعدة البيانات. يضمن ذلك عدم ظهور الملاحظة في طلبات البحث. يقدم الرمز التالي طلبًا صريحًا لإزالة مستند Note من قاعدة البيانات باستخدام رقم التعريف.

Kotlin

val removeRequest = RemoveByDocumentIdRequest.Builder("user1")
    .addIds("noteId")
    .build()

val removeFuture = Futures.transformAsync(
    sessionFuture, { session ->
        session?.remove(removeRequest)
    },
    mExecutor
)

Java

RemoveByDocumentIdRequest removeRequest = new RemoveByDocumentIdRequest.Builder("user1")
       .addIds("noteId")
       .build();

ListenableFuture<AppSearchBatchResult<String, Void>> removeFuture =
       Futures.transformAsync(sessionFuture, session -> session.remove(removeRequest), mExecutor);

الاحتفاظ بالقرص

يجب الاحتفاظ بتحديثات قاعدة البيانات بشكل دوري على القرص من خلال طلب requestFlush(). يستدعي الرمز التالي requestFlush() مع مستمع لتحديد ما إذا كانت المكالمة ناجحة.

Kotlin

val requestFlushFuture = Futures.transformAsync(
    sessionFuture,
    { session -> session?.requestFlush() }, mExecutor
)

Futures.addCallback(requestFlushFuture, object : FutureCallback<Void?> {
    override fun onSuccess(result: Void?) {
        // Success! Database updates have been persisted to disk.
    }

    override fun onFailure(t: Throwable) {
        Log.e(TAG, "Failed to flush database updates.", t)
    }
}, mExecutor)

Java

ListenableFuture<Void> requestFlushFuture = Futures.transformAsync(sessionFuture,
        session -> session.requestFlush(), mExecutor);

Futures.addCallback(requestFlushFuture, new FutureCallback<Void>() {
    @Override
    public void onSuccess(@Nullable Void result) {
        // Success! Database updates have been persisted to disk.
    }

    @Override
    public void onFailure(@NonNull Throwable t) {
        Log.e(TAG, "Failed to flush database updates.", t);
    }
}, mExecutor);

إغلاق جلسة

يجب إغلاق AppSearchSession عندما يتوقف التطبيق عن طلب أي عمليات لقاعدة البيانات. يُغلق الرمز التالي جلسة AppSearch التي تم فتحها سابقًا ويحتفظ بجميع التحديثات على القرص.

Kotlin

val closeFuture = Futures.transform<AppSearchSession, Unit>(sessionFuture,
    { session ->
        session?.close()
        Unit
    }, mExecutor
)

Java

ListenableFuture<Void> closeFuture = Futures.transform(sessionFuture, session -> {
   session.close();
   return null;
}, mExecutor);

مراجع إضافية

لمعرفة المزيد من المعلومات عن AppSearch، اطّلِع على المراجع الإضافية التالية:

عيّنات

  • نموذج Android AppSearch (Kotlin)، هو تطبيق لتدوين الملاحظات يستخدم AppSearch لفهرسة ملاحظات المستخدم ويسمح للمستخدمين بالبحث في ملاحظاتهم.

تقديم ملاحظات

يُرجى مشاركة ملاحظاتك وآرائك معنا من خلال الموارد التالية:

أداة تتبُّع المشاكل

الإبلاغ عن الأخطاء حتى نتمكّن من إصلاحها.