AppSearch

AppSearch là một giải pháp tìm kiếm hiệu suất cao trên thiết bị để quản lý dữ liệu có cấu trúc được lưu trữ cục bộ. Thư viện 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ể sử 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ó 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:

  • Phương thức triển khai bộ nhớ nhanh, ưu tiên thiết bị di động với mức sử dụng I/O thấp
  • Tạo chỉ mục và truy vấn hiệu quả cao trên các tập dữ liệu lớn
  • Hỗ trợ nhiều ngôn ngữ, chẳng hạn như tiếng Anh và tiếng Tây Ban Nha
  • Xếp hạng mức độ liên quan và tính năng tính điểm mức sử dụng

Do mức sử dụng I/O thấp hơn, AppSearch mang lại độ trễ thấp hơn để 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 trên nhiều loại bằng cách hỗ trợ các truy vấn đơn lẻ, trong khi SQLite 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 âm nhạc quản lý các bài hát yêu thích của người dùng 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 từ khắp nơi trên thế giới với tên bài hát bằng nhiều ngôn ngữ. AppSearch hỗ trợ lập chỉ mục và truy vấn các ngôn ngữ này ngay từ đầu. Khi người dùng tìm kiếm một bài hát theo tên hoặc tên nghệ sĩ, ứng dụng chỉ cần chuyển yêu cầu đến AppSearch để truy xuất nhanh chóng và hiệu quả các bài hát trùng khớp. Ứng dụng hiển thị kết quả, cho phép người dùng nhanh chóng bắt đầu phát bài hát yêu thích.

Thiết lập

Để sử dụng AppSearch trong ứng dụng, 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-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")
}

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

Sơ đồ sau đây minh hoạ các khái niệm của AppSearch và cách tương tác giữa các khái niệm đó.

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

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. Ứng dụng khách tạo cơ sở dữ liệu bằng cách cung cấp ngữ cảnh ứ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ơ sở dữ liệu. Khi mở một cơ sở dữ liệu, một phiên sẽ được trả về để tương tác với cơ sở dữ liệu đó. Phiên là điểm truy cập để gọi các API AppSearch và vẫn mở cho đến khi ứng dụng khách đóng phiên.

Giản đồ và loại giản đồ

Lược đồ thể hiện cấu trúc tổ chức của dữ liệu trong cơ sở dữ liệu AppSearch.

Giản đồ bao gồm các loại giản đồ đại diện cho các loại dữ liệu riêng biệt. Các loại giản đồ bao gồm các thuộc tính chứa tên, loại dữ liệu và số lượng giá trị riêng biệt. Sau khi thêm một loại giản đồ vào giản đồ cơ sở dữ liệu, bạn có thể tạo và thêm các tài liệu thuộc loại giản đồ đó 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 cơ sở dữ liệu AppSearch được xác định duy nhất bằng không gian tên và mã nhận dạng. Không gian tên được dùng để tách dữ liệu từ các nguồn khác nhau 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ể được dùng để xếp hạng trong quá trình truy xuất. Một tài liệu cũng được chỉ định 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 được 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 cho loại giản đồ.

Các tài liệu được lập chỉ mục và có thể được tìm kiếm bằng cách cung cấp một truy vấn. Một tài liệu sẽ được khớp và đưa vào kết quả tìm kiếm nếu tài liệu đó chứa các cụm từ trong truy vấn hoặc khớp với một thông số kỹ thuật tìm kiếm khác. Các kết quả được sắp xếp dựa trên điểm số và chiến lược xếp 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 theo tuần tự.

AppSearch cung cấp các tính năng tuỳ chỉnh cho tính nă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.

Bộ nhớ nền tảng, Bộ nhớ cục bộ hoặc Bộ nhớ Dịch vụ Play

AppSearch cung cấp ba giải pháp lưu trữ: LocalStorage, PlatformStoragePlayServicesStorage. Với LocalStorage, ứng dụng của bạn sẽ 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 cả PlatformStoragePlayServicesStorage, ứng dụng của bạn sẽ đóng góp vào một chỉ mục trung tâm trên toàn hệ thống. Chỉ mục của PlatformStorage được lưu trữ trong máy chủ hệ thống và chỉ mục của PlayServicesStorage được lưu trữ trong bộ nhớ của Dịch vụ Google Play. Quyền truy cập dữ liệu trong các chỉ mục trung tâm này chỉ được cấp cho dữ liệu mà ứng dụng của bạn đã đóng góp và dữ liệu mà một ứng dụng khác đã chia sẻ rõ ràng với bạn. Tất cả các tuỳ chọn bộ nhớ này đều dùng chung một API và có thể được hoán đổi 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 {
    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()));
    }
}

Khi sử dụng PlatformStoragePlayServicesStorage, ứ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 trong dữ liệu của ứng dụng. Việc chia sẻ dữ liệu ứng dụng chỉ có thể đọc được cấp bằng cách bắt tay chứng chỉ để đảm bảo rằng ứng dụng khác có quyền đọc dữ liệu. Đọc thêm về API này trong tài liệu về setSchemaTypeVisibilityForPackage().

Ngoài ra, với PlatformStorage, 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. Các ứng dụng có thể chọn không hiển thị một số hoặc tất cả dữ liệu của chúng 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 về setSchemaTypeDisplayedBySystem().

Tính năng LocalStorage (tương thích với Android 5.0 trở lên) PlatformStorage (tương thích với Android 12 trở lên) PlayServicesStorage (tương thích với Android 5.0 trở lên)
Tìm kiếm toàn bộ văn bản hiệu quả
Hỗ trợ nhiều ngôn ngữ
Giảm kích thước tệp nhị phân
Chia sẻ dữ liệu giữa các ứng dụng
Khả năng hiển thị dữ liệu trên các nền tảng Giao diện người dùng hệ thống
Có thể lập chỉ mục số lượng và kích thước tài liệu không giới hạn
Các thao tác nhanh hơn mà không cần thêm độ trễ liên kết

Có một số điểm đánh đổi khác cần cân nhắc khi chọn giữa LocalStoragePlatformStorage. Vì PlatformStorage gói các API Jetpack qua 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 thao tác AppSearch sẽ 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 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 hoạt động hiệu quả. PlayServicesStorage cũng có các giới hạn giống như PlatformStorage và chỉ được hỗ trợ trên các thiết bị có Dịch vụ Google Play.

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

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

Viế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 thực thể 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.

Đoạn mã sau đây xác định một lớp tài liệu Note (Ghi chú) có trường chú thích @Document.StringProperty để lập chỉ mục văn bản của đối tượng 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;
  }
}

Mở cơ sở dữ liệu

Bạn phải tạo cơ sở dữ liệu trước khi làm việc với tài liệu. Mã sau đây tạo một cơ sở dữ liệu mới có tên notes_app và nhận ListenableFuture cho AppSearchSession, đại diện cho kết nối với cơ sở dữ liệu và cung cấp các API cho các thao tác 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 thiết lập 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 đồ". Mã sau đây đặt giản đồ bằng cách cung cấp lớp tài liệu dưới dạng loại 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);

Đưa 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 một tài liệu thuộc loại giản đồ Note bằng trình tạo lớp tài liệu Note. Hàm này đặ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 được chèn vào cơ sở dữ liệu và một trình nghe đượ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 các tài liệu được lập chỉ mục bằ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ừ "fruit" (trái cây) trên cơ sở dữ liệu cho 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 qua SearchResults

Các lượ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 chứa GenericDocument đã so khớp, đây là dạng chung của tài liệu mà tất cả tài liệu được chuyển đổi sang. Mã sau đây sẽ lấy trang kết quả tìm kiếm đầu tiên 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. Thao tác này đảm bảo rằng ghi chú sẽ không còn xuất hiện trong các truy vấn. Mã sau đây đưa ra yêu cầu rõ ràng để xoá tài liệu Note khỏi cơ sở dữ liệu theo mã nhận dạng.

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

Lưu vào ổ đĩa

Bạn nên định kỳ lưu các nội dung cập nhật vào cơ sở dữ liệu bằng cách gọi requestFlush(). Mã sau đây gọi requestFlush() bằng 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 nên đóng AppSearchSession khi ứng dụng không còn gọi bất kỳ thao tác cơ sở dữ liệu nào nữa. Mã sau đây sẽ đóng phiên AppSearch đã mở trước đó và lưu tất cả nội dung 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 các tài nguyên bổ sung sau:

Mẫu

  • Mẫu AppSearch cho Android (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 trong 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.