AppSearch

AppSearch là một giải pháp tìm kiếm trên thiết bị có hiệu suất cao để quản lý dữ liệu có cấu trúc được lưu trữ cục bộ. Lớp này chứa các API để lập chỉ mục dữ liệu và truy xuất dữ liệu bằng phương thức tìm kiếm toàn bộ văn bản. Các ứng dụng có thể dùng AppSearch để cung cấp các chức năng tìm kiếm tuỳ chỉnh trong ứng dụng, cho phép người dùng tìm kiếm nội dung ngay cả khi không có kết nối mạng.

Sơ đồ minh hoạ việc lập chỉ mục và tìm kiếm trong AppSearch

AppSearch cung cấp các tính năng sau:

  • Triển khai bộ nhớ nhanh chóng và ưu tiên thiết bị di động mà vẫn sử dụng I/O thấp
  • Lập chỉ mục và truy vấn rất hiệu quả trên các tập dữ liệu lớn
  • Hỗ trợ đa ngôn ngữ, chẳng hạn như tiếng Anh và tiếng Tây Ban Nha
  • Điểm xếp hạng mức độ liên quan và mức độ sử dụng

Do sử dụng I/O thấp hơn, AppSearch cung cấp độ trễ thấp hơn cho việc lập chỉ mục và tìm kiếm trên các tập dữ liệu lớn so với SQLite. AppSearch đơn giản hoá các truy vấn nhiều loại bằng cách hỗ trợ các truy vấn đơn, trong khi SQLite sẽ hợp nhất kết quả từ nhiều bảng.

Để minh hoạ các tính năng của AppSearch, hãy lấy ví dụ về một ứng dụng nhạc quản lý các bài hát mà người dùng yêu thích và cho phép người dùng dễ dàng tìm kiếm các bài hát đó. Người dùng thưởng thức âm nhạc ở khắp nơi trên thế giới thông qua tên bài hát bằng nhiều ngôn ngữ. Theo đó, AppSearch vốn đã hỗ trợ lập chỉ mục và truy vấn. Khi người dùng tìm kiếm một bài hát theo tiêu đề hoặc tên nghệ sĩ, ứng dụng chỉ cần chuyển yêu cầu đến AppSearch để truy xuất các bài hát trùng khớp một cách nhanh chóng và hiệu quả. Ứng dụng hiển thị các kết quả, cho phép người dùng nhanh chóng bắt đầu phát các bài hát họ yêu thích.

Thiết lập

Để dùng AppSearch trong ứng dụng của bạn, hãy thêm các phần phụ thuộc sau vào tệp build.gradle của ứng dụng:

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")
}

Các khái niệm về AppSearch

Sơ đồ dưới đây minh hoạ các khái niệm trong AppSearch và hoạt động tương tác với các khái niệm đó.

Sơ đồ quy trình của một ứng dụng khách và hoạt động tương tác của ứng dụng đó với các khái niệm sau của AppSearch: cơ sở dữ liệu AppSearch, giản đồ, loại giản đồ, tài liệu, phiên và nội dung tìm kiếm. Hình 1. Sơ đồ về các khái niệm của AppSearch: cơ sở dữ liệu, giản đồ, loại giản đồ, tài liệu, phiên và nội dung tìm kiếm của AppSearch.

Cơ sở dữ liệu và phiên

Cơ sở dữ liệu AppSearch là một tập hợp các tài liệu tuân thủ giản đồ cơ sở dữ liệu. Các ứng dụng khách tạo cơ sở dữ liệu bằng cách cung cấp ngữ cảnh của ứng dụng và tên cơ sở dữ liệu. Chỉ ứng dụng đã tạo cơ sở dữ liệu mới có thể mở được cơ sở dữ liệu đó. Khi cơ sở dữ liệu được mở, một phiên sẽ được trả về để tương tác với cơ sở dữ liệu. Phiên hoạt động là điểm truy cập để gọi các API AppSearch và vẫn mở cho đến khi bị ứng dụng đóng.

Loại giản đồ và giản đồ

Giản đồ thể hiện cơ cấu tổ chức của dữ liệu trong cơ sở dữ liệu AppSearch.

Giản đồ này bao gồm các loại giản đồ đại diện cho các loại dữ liệu riêng biệt. Loại giản đồ bao gồm các thuộc tính chứa tên, loại dữ liệu và lượng số. Sau khi một loại giản đồ được thêm vào giản đồ cơ sở dữ liệu, các tài liệu thuộc loại giản đồ đó có thể được tạo và thêm vào cơ sở dữ liệu.

Tài liệu

Trong AppSearch, một đơn vị dữ liệu được biểu thị dưới dạng một tài liệu. Mỗi tài liệu trong một cơ sở dữ liệu AppSearch được xác định riêng biệt theo không gian tên và mã nhận dạng của tài liệu đó. Không gian tên được dùng để phân tách dữ liệu từ nhiều nguồn khi chỉ cần truy vấn một nguồn, chẳng hạn như tài khoản người dùng.

Tài liệu chứa dấu thời gian tạo, thời gian tồn tại (TTL) và điểm số có thể dùng để xếp hạng trong quá trình truy xuất. Tài liệu cũng được gán một loại giản đồ mô tả các thuộc tính dữ liệu bổ sung mà tài liệu phải có.

Lớp tài liệu là một bản tóm tắt của tài liệu. Tệp này chứa các trường chú thích đại diện cho nội dung của tài liệu. Theo mặc định, tên của lớp tài liệu sẽ đặt tên của loại giản đồ.

Các tài liệu được lập chỉ mục và có thể tìm kiếm được bằng cách cung cấp truy vấn. Một tài liệu sẽ được so khớp và đưa vào kết quả tìm kiếm nếu tài liệu đó chứa các từ khoá trong cụm từ tìm kiếm hoặc khớp với một quy cách tìm kiếm khác. Kết quả được sắp xếp dựa trên chiến lược về điểm số và thứ hạng. Kết quả tìm kiếm được biểu thị bằng các trang mà bạn có thể truy xuất tuần tự.

AppSearch cung cấp các tuỳ chỉnh cho hoạt động tìm kiếm, chẳng hạn như bộ lọc, cấu hình kích thước trang và trích đoạn nội dung.

Bộ nhớ nền tảng so với Bộ nhớ cục bộ

AppSearch cung cấp hai giải pháp lưu trữ: LocalStorage và PlatformStorage Với LocalStorage, ứng dụng của bạn quản lý một chỉ mục dành riêng cho ứng dụng nằm trong thư mục dữ liệu ứng dụng. Với PlatformStorage, ứng dụng của bạn đóng góp vào chỉ mục trung tâm trên toàn hệ thống. Quyền truy cập dữ liệu trong chỉ mục trung tâm bị hạn chế ở dữ liệu mà ứng dụng của bạn đã đóng góp và dữ liệu đã được một ứng dụng khác chia sẻ rõ ràng với bạn. Cả LocalStorage và PlatformStorage đều có cùng API và có thể thay thế cho nhau dựa trên phiên bản của thiết bị:

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

Khi sử dụng PlatformStorage, ứng dụng của bạn có thể chia sẻ dữ liệu một cách an toàn với các ứng dụng khác để cho phép các ứng dụng đó tìm kiếm trên cả dữ liệu ứng dụng của bạn. Tính năng chia sẻ dữ liệu ứng dụng ở chế độ chỉ có thể đọc được cấp thông qua quá trình bắt tay chứng chỉ để đảm bảo rằng ứng dụng khác có quyền đọc dữ liệu. Hãy đọc thêm về API này trong tài liệu về setSchemaTypeVisibilityForPackage().

Ngoài ra, dữ liệu đã được lập chỉ mục có thể hiển thị trên các nền tảng Giao diện người dùng hệ thống. Ứng dụng có thể chọn không hiển thị một số hoặc tất cả dữ liệu trên các nền tảng giao diện người dùng hệ thống. Đọc thêm về API này trong tài liệu dành cho setSchemaTypeDisplayedBySystem().

Tính năng 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

Bạn cũng cần cân nhắc một số yếu tố đánh đổi khác khi chọn giữa LocalStorage và PlatformStorage. Vì PlatformStorage bao bọc API Jetpack thay vì dịch vụ hệ thống AppSearch, nên tác động đến kích thước APK là tối thiểu so với việc sử dụng LocalStorage. Tuy nhiên, điều này cũng có nghĩa là các hoạt động AppSearch phải chịu thêm độ trễ liên kết khi gọi dịch vụ hệ thống AppSearch. Với PlatformStorage, AppSearch giới hạn số lượng tài liệu và kích thước của tài liệu mà một ứng dụng có thể lập chỉ mục để đảm bảo chỉ mục trung tâm hiệu quả.

Bắt đầu sử dụng AppSearch

Ví dụ trong phần này giới thiệu cách sử dụng API AppSearch để tích hợp với một ứng dụng lưu giữ ghi chú giả định.

Viết một lớp tài liệu

Bước đầu tiên để tích hợp với AppSearch là viết một lớp tài liệu để mô tả dữ liệu cần chèn vào cơ sở dữ liệu. Đánh dấu một lớp là lớp tài liệu bằng cách sử dụng chú giải @Document.Bạn có thể sử dụng các bản sao của lớp tài liệu để đưa tài liệu vào và truy xuất tài liệu từ cơ sở dữ liệu.

Mã sau đây xác định một lớp tài liệu Ghi chú có trường chú thích @Document.StringProperty để lập chỉ mục văn bản của đối tượng Ghi chú.

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

Mở cơ sở dữ liệu

Bạn phải tạo một cơ sở dữ liệu trước khi làm việc với tài liệu. Đoạn mã sau đây sẽ tạo một cơ sở dữ liệu mới có tên là notes_app và nhận ListenableFuture cho AppSearchSession, đại diện cho sự kết nối với cơ sở dữ liệu và cung cấp các API cho các thao tác với cơ sở dữ liệu.

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

Đặt giản đồ

Bạn phải đặt một giản đồ trước khi có thể đưa tài liệu vào và truy xuất tài liệu từ cơ sở dữ liệu. Giản đồ cơ sở dữ liệu bao gồm nhiều loại dữ liệu có cấu trúc, được gọi là "loại giản đồ". Đoạn mã sau đây đặt giản đồ bằng cách cung cấp lớp tài liệu dưới dạng một kiểu giản đồ.

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

Đặt tài liệu vào cơ sở dữ liệu

Sau khi thêm một loại giản đồ, bạn có thể thêm các tài liệu thuộc loại đó vào cơ sở dữ liệu. Mã sau đây tạo tài liệu thuộc loại giản đồ Note bằng cách sử dụng trình tạo lớp tài liệu Note. Thao tác này sẽ đặt không gian tên tài liệu user1 để đại diện cho một người dùng tuỳ ý của mẫu này. Sau đó, tài liệu này được chèn vào cơ sở dữ liệu và trình nghe sẽ được đính kèm để xử lý kết quả của thao tác đặt.

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

Bạn có thể tìm kiếm tài liệu đã được lập chỉ mục bằng cách sử dụng các thao tác tìm kiếm được đề cập trong phần này. Mã sau đây thực hiện truy vấn cho cụm từ "trái cây" trên cơ sở dữ liệu đối với các tài liệu thuộc không gian tên 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);

Lặp lại thông qua SearchResults

Các cụm từ tìm kiếm sẽ trả về một thực thể SearchResults, cho phép truy cập vào các trang của đối tượng SearchResult. Mỗi SearchResult sẽ chứa GenericDocument trùng khớp – dạng chung của tài liệu mà mọi tài liệu đều được chuyển đổi sang. Đoạn mã sau đây sẽ tải trang đầu tiên của kết quả tìm kiếm và chuyển đổi kết quả trở lại thành tài liệu 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);

Xoá tài liệu

Khi người dùng xoá một ghi chú, ứng dụng sẽ xoá tài liệu Note tương ứng khỏi cơ sở dữ liệu. Điều này đảm bảo ghi chú sẽ không còn xuất hiện trong các truy vấn nữa. Mã sau đây đưa ra một yêu cầu rõ ràng về việc xoá tài liệu Note khỏi cơ sở dữ liệu theo mã nhận dạng (Id).

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

Duy trì vào ổ đĩa

Các bản cập nhật cho cơ sở dữ liệu phải được lưu trữ định kỳ trên ổ đĩa bằng cách gọi requestFlush(). Đoạn mã sau đây gọi requestFlush() bằng một trình nghe để xác định xem lệnh gọi có thành công hay không.

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

Đóng phiên

Bạn sẽ đóng AppSearchSession khi một ứng dụng sẽ không gọi bất kỳ thao tác nào với cơ sở dữ liệu nữa. Mã sau đây đóng phiên AppSearch đã mở trước đó và duy trì tất cả bản cập nhật vào ổ đĩa.

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

Tài nguyên khác

Để tìm hiểu thêm về AppSearch, hãy xem thêm các tài nguyên sau đây:

Mẫu

  • Android AppSearch Sample (Kotlin), một ứng dụng ghi chú sử dụng AppSearch để lập chỉ mục ghi chú của người dùng và cho phép người dùng tìm kiếm trên các ghi chú của họ.

Gửi ý kiến phản hồi

Hãy chia sẻ phản hồi và ý kiến của bạn với chúng tôi thông qua các tài nguyên sau:

Công cụ theo dõi lỗi

Báo cáo lỗi để chúng tôi có thể khắc phục.