חיפוש אפליקציות

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

סוגי סכימות וסכימות

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

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

מסמכים

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

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

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

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

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

אחסון פלטפורמה לעומת אחסון מקומי

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

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

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

תחילת העבודה עם AppSearch

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

כתיבת כיתה למסמכים

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

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

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);

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

שליחת משוב

אפשר לשתף איתנו משוב ורעיונות דרך המשאבים הבאים:

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

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