Поиск приложений

AppSearch — это высокопроизводительное решение для поиска на устройстве для управления локально хранящимися структурированными данными. Он содержит API для индексации данных и получения данных с помощью полнотекстового поиска. Приложения могут использовать AppSearch для предоставления настраиваемых возможностей поиска внутри приложения, позволяя пользователям искать контент даже в автономном режиме.

Диаграмма, иллюстрирующая индексирование и поиск в AppSearch

AppSearch предоставляет следующие возможности:

  • Быстрая реализация хранилища, ориентированная на мобильные устройства, с низким использованием операций ввода-вывода.
  • Высокоэффективная индексация и запросы к большим наборам данных.
  • Многоязычная поддержка, например английский и испанский.
  • Рейтинг релевантности и оценка использования

Благодаря меньшему использованию ввода-вывода AppSearch предлагает меньшую задержку для индексации и поиска по большим наборам данных по сравнению с SQLite. AppSearch упрощает запросы перекрестного типа, поддерживая одиночные запросы, тогда как SQLite объединяет результаты из нескольких таблиц.

Чтобы проиллюстрировать возможности AppSearch, давайте возьмем пример музыкального приложения, которое управляет любимыми песнями пользователей и позволяет пользователям легко их искать. Пользователи наслаждаются музыкой со всего мира с названиями песен на разных языках, для которых AppSearch изначально поддерживает индексацию и запросы. Когда пользователь ищет песню по названию или имени исполнителя, приложение просто передает запрос в AppSearch, чтобы быстро и эффективно найти подходящие песни. Приложение отображает результаты, позволяя пользователям быстро начать воспроизводить свои любимые песни.

Настраивать

Чтобы использовать AppSearch в своем приложении, добавьте следующие зависимости в файл build.gradle вашего приложения:

Groovy

dependencies {
    def appsearch_version = "1.1.0-alpha06"

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

    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, схема, типы схем, документы, сеанс и поиск. Рис. 1. Схема концепций AppSearch: база данных AppSearch, схема, типы схем, документы, сеанс и поиск.

База данных и сеанс

База данных AppSearch — это набор документов, соответствующий схеме базы данных. Клиентские приложения создают базу данных, предоставляя контекст своего приложения и имя базы данных. Базы данных могут быть открыты только приложением, которое их создало. Когда база данных открывается, возвращается сеанс для взаимодействия с базой данных. Сеанс является точкой входа для вызова API AppSearch и остается открытым до тех пор, пока не будет закрыт клиентским приложением.

Схема и типы схем

Схема представляет собой организационную структуру данных в базе данных AppSearch.

Схема состоит из типов схемы, которые представляют уникальные типы данных. Типы схем состоят из свойств, которые содержат имя, тип данных и мощность. После добавления типа схемы в схему базы данных можно создавать и добавлять в базу данных документы этого типа схемы.

Документы

В AppSearch единица данных представлена ​​в виде документа. Каждый документ в базе данных AppSearch уникально идентифицируется своим пространством имен и идентификатором. Пространства имен используются для разделения данных из разных источников, когда необходимо запросить только один источник, например учетные записи пользователей.

Документы содержат отметку времени создания, срок жизни (TTL) и оценку, которую можно использовать для ранжирования во время поиска. Документу также назначается тип схемы, который описывает дополнительные свойства данных, которые должен иметь документ.

Класс документа — это абстракция документа. Он содержит аннотированные поля, которые представляют содержимое документа. По умолчанию имя класса документа задает имя типа схемы.

Документы индексируются, и их можно найти, указав запрос. Документ сопоставляется и включается в результаты поиска, если он содержит термины запроса или соответствует другой спецификации поиска. Результаты упорядочены на основе их оценки и стратегии ранжирования. Результаты поиска представлены страницами, которые вы можете просматривать последовательно.

AppSearch предлагает настройки поиска, такие как фильтры, конфигурация размера страницы и фрагменты.

Платформенное хранилище против локального хранилища

AppSearch предлагает два решения для хранения данных: LocalStorage и PlatformStorage. С помощью LocalStorage ваше приложение управляет индексом, специфичным для приложения, который находится в каталоге данных вашего приложения. Благодаря PlatformStorage ваше приложение вносит свой вклад в общесистемный центральный индекс. Доступ к данным в центральном индексе ограничен данными, предоставленными вашим приложением, и данными, которые были явно предоставлены вам другим приложением. И LocalStorage, и PlatformStorage используют один и тот же API и могут быть взаимозаменяемыми в зависимости от версии устройства:

Котлин

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

Ява

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 вызывают дополнительную задержку связывания при вызове системной службы AppSearch. С помощью PlatformStorage AppSearch ограничивает количество и размер документов, которые приложение может индексировать, чтобы обеспечить эффективный централизованный индекс.

Начните работу с AppSearch

В примере в этом разделе показано, как использовать API AppSearch для интеграции с гипотетическим приложением для ведения заметок.

Напишите класс документа

Первым шагом для интеграции с AppSearch является написание класса документа, описывающего данные, которые необходимо вставить в базу данных. Пометьте класс как класс документа, используя аннотацию @Document . Вы можете использовать экземпляры класса документа для помещения документов в базу данных и получения документов из базы данных.

Следующий код определяет класс документа Note с аннотированным полем @Document.StringProperty для индексации текста объекта Note.

Котлин

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

Ява

@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 для операций с базой данных.

Котлин

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

Ява

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

Установить схему

Вы должны установить схему, прежде чем сможете помещать документы в базу данных и извлекать их из базы данных. Схема базы данных состоит из различных типов структурированных данных, называемых «типами схемы». Следующий код устанавливает схему, предоставляя класс документа в качестве типа схемы.

Котлин

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

Ява

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

Поместить документ в базу данных

После добавления типа схемы вы можете добавлять документы этого типа в базу данных. Следующий код создает документ типа схемы Note с помощью построителя классов документов Note . Он устанавливает пространство имен документа user1 для представления произвольного пользователя из этого примера. Затем документ вставляется в базу данных, и к нему подключается прослушиватель для обработки результата операции размещения.

Котлин

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
)

Ява

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 .

Котлин

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
)

Ява

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 .

Котлин

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
)

Ява

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 из базы данных по идентификатору.

Котлин

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

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

Ява

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

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

Сохранить на диск

Обновления базы данных следует периодически сохранять на диск путем вызова requestFlush() . Следующий код вызывает requestFlush() с прослушивателем, чтобы определить, был ли вызов успешным.

Котлин

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)

Ява

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, который был открыт ранее, и сохраняет все обновления на диске.

Котлин

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

Ява

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

Дополнительные ресурсы

Чтобы узнать больше о AppSearch, посетите следующие дополнительные ресурсы:

Образцы

  • Пример Android AppSearch (Kotlin) — приложение для создания заметок, которое использует AppSearch для индексации заметок пользователя и позволяет пользователям выполнять поиск по своим заметкам.

Оставьте отзыв

Поделитесь с нами своими отзывами и идеями через эти ресурсы:

Трекер проблем

Сообщайте об ошибках, чтобы мы могли их исправить.