AppSearch

‏AppSearch הוא פתרון חיפוש במכשיר עם ביצועים גבוהים לניהול נתונים מובְנים שמאוחסנים באופן מקומי. הוא מכיל ממשקי API להוספת נתונים לאינדקס ולאחזור נתונים באמצעות חיפוש טקסט מלא. אפליקציות יכולות להשתמש ב-AppSearch כדי להציע יכולות חיפוש בהתאמה אישית בתוך האפליקציה, שמאפשרות למשתמשים לחפש תוכן גם במצב אופליין.

תרשים שממחיש הוספה לאינדקס וחיפוש ב-AppSearch

AppSearch מספק את התכונות הבאות:

  • הטמעה מהירה של אחסון שמתאימה במיוחד לנייד, עם שימוש נמוך ב-I/O
  • הוספה של נתונים לאינדקס ושליחת שאילתות יעילות במיוחד על קבוצות נתונים גדולות
  • תמיכה במספר שפות, כמו אנגלית וספרדית
  • דירוג רלוונטיות וסימון שימוש

בגלל השימוש הנמוך יותר ב-I/O, זמן האחזור של AppSearch להוספה לאינדקס ולחיפוש במערכי נתונים גדולים קצר יותר בהשוואה ל-SQLite. AppSearch מפשט שאילתות מסוגים שונים על ידי תמיכה בשאילתות יחידות, בעוד ש-SQLite ממזג תוצאות ממספר טבלאות.

כדי להמחיש את התכונות של AppSearch, ניקח לדוגמה אפליקציית מוזיקה שמנהלת את השירים האהובים של המשתמשים ומאפשרת להם לחפש אותם בקלות. המשתמשים נהנים ממוזיקה מרחבי העולם עם שמות שירים בשפות שונות, ו-AppSearch תומך באופן מקורי בהוספה שלהם לאינדקס ובשליחת שאילתות לגביהם. כשהמשתמש מחפש שיר לפי שם או לפי שם האומן, האפליקציה מעבירה את הבקשה ל-AppSearch כדי לאחזר שירים תואמים במהירות וביעילות. התוצאות מוצגות באפליקציה, ומאפשרות למשתמשים להתחיל להשמיע את השירים האהובים עליהם במהירות.

הגדרה

כדי להשתמש ב-AppSearch באפליקציה, מוסיפים את יחסי התלות הבאים לקובץ build.gradle של האפליקציה:

מגניב

dependencies {
    def appsearch_version = "1.1.0-alpha05"

    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-alpha05"

    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 הוא אוסף של מסמכים שתואמים לסכימה של מסד הנתונים. אפליקציות לקוח יוצרות מסד נתונים על ידי מתן ההקשר של האפליקציה ושם של מסד נתונים. רק האפליקציה שיצרה אותן יכולה לפתוח את מסדי הנתונים. כשפותחים מסד נתונים, מוחזר סשן כדי לבצע אינטראקציה עם מסד הנתונים. הסשן הוא נקודת הכניסה לקריאה ל-API של AppSearch, והוא נשאר פתוח עד שאפליקציית הלקוח סוגרת אותו.

סכימות וסוגים של סכימות

סכמה מייצגת את המבנה הארגוני של הנתונים במסד הנתונים של AppSearch.

הסכימה מורכבת מסוגים של סכימה שמייצגים סוגים ייחודיים של נתונים. סוגי סכימות מורכבים ממאפיינים שמכילים שם, סוג נתונים ועושר יחסים. אחרי שמוסיפים סוג סכימה לסכימה של מסד הנתונים, אפשר ליצור מסמכים מסוג הסכימה הזה ולהוסיף אותם למסד הנתונים.

מסמכים

ב-AppSearch, יחידת נתונים מיוצגת כמסמך. כל מסמך במסד נתונים של AppSearch מזוהה באופן ייחודי באמצעות המרחב השם והמזהה שלו. מרחבי שמות משמשים להפרדת נתונים ממקורות שונים, כשצריך לשלוח שאילתה רק למקור אחד, כמו חשבונות משתמשים.

המסמכים מכילים חותמת זמן ליצירה, אורך חיים (TTL) ודירוג שאפשר להשתמש בו לדירוג במהלך אחזור. למסמך מוקצה גם סוג סכימה שמתאר מאפייני נתונים נוספים שהמסמך חייב לכלול.

סיווג מסמך הוא הפשטה של מסמך. הוא מכיל שדות עם הערות שמייצגים את תוכן המסמך. כברירת מחדל, השם של סוג המסמך מגדיר את השם של סוג הסכימה.

המסמכים נוספים לאינדקס וניתן לחפש אותם באמצעות שאילתות. מסמך נחשב כמתאים ומוכלל בתוצאות החיפוש אם הוא מכיל את המונחים בשאילתה או תואם למפרט חיפוש אחר. התוצאות ממוינות לפי הציון שלהן ושיטה הדירוג. תוצאות החיפוש מיוצגות על ידי דפים שאפשר לאחזר ברצף.

ב-AppSearch יש התאמות אישיות לחיפוש, כמו מסננים, הגדרת גודל דף יצירת קטעי קוד.

אחסון בפלטפורמה, אחסון מקומי או אחסון ב-Play Services

ב-AppSearch יש שלושה פתרונות אחסון: LocalStorage, ‏ PlatformStorage ו-PlayServicesStorage. באמצעות LocalStorage, האפליקציה מנהלת אינדקס ספציפי לאפליקציה שנמצא בתיקיית נתוני האפליקציה. כשמשתמשים גם ב-PlatformStorage וגם ב-PlayServicesStorage, האפליקציה תורמת לאינדקס מרכזי ברמת המערכת. האינדקס של PlatformStorage מתארח בשרת המערכת, והאינדקס של PlayServicesStorage מתארח באחסון של Google Play Services. הגישה לנתונים בתוך המדדים המרכזיים האלה מוגבלת לנתונים שהאפליקציה שלכם תרמה ונתונים שאפליקציה אחרת שיתפה איתכם באופן מפורש. לכל אפשרויות האחסון האלה יש את אותו ממשק API, ואפשר להחליף ביניהן בהתאם לגרסה של המכשיר:

Kotlin

if (BuildCompat.isAtLeastS()) {
    appSearchSessionFuture.setFuture(
        PlatformStorage.createSearchSession(
            PlatformStorage.SearchContext.Builder(mContext, DATABASE_NAME)
               .build()
        )
    )
} else {
    if (usePlayServicesStorageBelowS) {
        appSearchSessionFuture.setFuture(
            PlayServicesStorage.createSearchSession(
                PlayServicesStorage.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 {
    if (usePlayServicesStorageBelowS) {
        mAppSearchSessionFuture.setFuture(PlayServicesStorage.createSearchSession(
                new PlayServicesStorage.SearchContext.Builder(mContext, DATABASE_NAME)
                        .build()));
    } else {
        mAppSearchSessionFuture.setFuture(LocalStorage.createSearchSession(
                new LocalStorage.SearchContext.Builder(mContext, DATABASE_NAME)
                        .build()));
    }
}

באמצעות PlatformStorage ו-PlayServicesStorage, האפליקציה שלכם יכולה לשתף נתונים באופן מאובטח עם אפליקציות אחרות, כדי לאפשר להן לחפש גם בנתונים של האפליקציה. שיתוף נתוני אפליקציה לקריאה בלבד מותנה בהשתמש בלחיצת יד של אישור כדי לוודא שלאפליקציה השנייה יש הרשאה לקרוא את הנתונים. מידע נוסף על ה-API הזה זמין במסמכי העזרה של setSchemaTypeVisibilityForPackage().

בנוסף, בעזרת PlatformStorage אפשר להציג נתונים שנוספו לאינדקס בממשקי System UI. אפליקציות יכולות לבקש שלא יוצגו חלק מהנתונים שלהן או כולם בממשק המשתמש של המערכת. מידע נוסף על ה-API הזה זמין במסמכי התיעוד של setSchemaTypeDisplayedBySystem().

תכונות LocalStorage (תואם ל-Android 5.0 ואילך) PlatformStorage (תואם ל-Android 12 ואילך) PlayServicesStorage (תואם ל-Android 5.0 ואילך)
חיפוש טקסט מלא יעיל
תמיכה בכמה שפות
גודל בינארי מופחת
שיתוף נתונים בין אפליקציות
יכולת להציג נתונים בממשקי המשתמש של המערכת
אפשר להוסיף לאינדקס מסמכים בכמות ובגודל בלתי מוגבלים
פעולות מהירות יותר ללא זמן אחזור נוסף של הקישור

יש עוד שיקולים שצריך להביא בחשבון כשבוחרים בין LocalStorage לבין PlatformStorage. מכיוון ש-PlatformStorage עוטף את ממשקי Jetpack API בשירות המערכת של AppSearch, ההשפעה על גודל ה-APK היא מינימלית בהשוואה לשימוש ב-LocalStorage. עם זאת, המשמעות היא גם שלפעולות AppSearch יש זמן אחזור נוסף של הקישור כשקוראים לשירות המערכת של AppSearch. בעזרת PlatformStorage, ‏AppSearch מגביל את מספר המסמכים ואת הגודל של המסמכים שאפשר להוסיף לאינדקס של האפליקציה, כדי להבטיח אינדקס מרכזי יעיל. גם ל-PlayServicesStorage יש את אותן המגבלות כמו ל-PlatformStorage, והיא נתמכת רק במכשירים עם Google Play Services.

תחילת השימוש ב-AppSearch

בדוגמה שבקטע הזה נסביר איך משתמשים בממשקי ה-API של AppSearch כדי לשלב את השירות עם אפליקציה היפותטית לניהול הערות.

כתיבת סוג מסמך

השלב הראשון בשילוב עם AppSearch הוא לכתוב סיווג מסמכים כדי לתאר את הנתונים שרוצים להוסיף למסד הנתונים. מסמנים כיתה ככיתה של מסמכים באמצעות ההערה @Document.אפשר להשתמש במופעים של כיתת המסמכים כדי להוסיף מסמכים למסד הנתונים ולשלוף מסמכים ממנו.

הקוד הבא מגדיר סוג מסמך של Note עם שדה הערה @Document.StringProperty להוספה של טקסט של אובייקט Note לאינדקס.

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, שמייצג את החיבור למסד הנתונים ומספק את ממשקי ה-API לפעולות במסד הנתונים.

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 באמצעות ה-builder של סוג המסמך Note. הוא מגדיר את מרחב השמות של המסמך user1 כדי לייצג משתמש שרירותי בדוגמה הזו. לאחר מכן המסמך מוכנס למסד הנתונים ומצורף מאזין כדי לעבד את התוצאה של פעולת ה-put.

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

החיפושים מחזירים מופע של 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 Sample (Kotlin), אפליקציה לניהול הערות שמשתמשת ב-AppSearch כדי להוסיף הערות של משתמשים לאינדקס ולאפשר למשתמשים לחפש בהן.

שליחת משוב

אתם יכולים לשתף איתנו את המשוב והרעיונות שלכם באמצעות המשאבים הבאים:

מעקב אחר בעיות

דיווח על באגים כדי שנוכל לתקן אותם.