AppSearch

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

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

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

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

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

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

Настроить

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

Яркий

dependencies {
    def appsearch_version = "1.2.0-alpha02"

    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 supports sharing data
    // with other apps. Feature support varies by device. Before calling an API that
    // requires a feature, check Features.isFeatureSupported().
    implementation "androidx.appsearch:appsearch-platform-storage:$appsearch_version"

    // PlayServicesStorage is compatible with all devices that support Google Play Services on
    // all API levels. It offers similar capabilities to PlatformStorage and is the
    // recommended solution for lower API levels on which PlatformStorage is not supported.
    implementation "androidx.appsearch:appsearch-play-services-storage:$appsearch_version"
}

Kotlin

dependencies {
    val appsearch_version = "1.2.0-alpha02"

    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 supports sharing data
    // with other apps. Feature support varies by device. Before calling an API that
    // requires a feature, check Features.isFeatureSupported().
    implementation("androidx.appsearch:appsearch-platform-storage:$appsearch_version")

    // PlayServicesStorage is compatible with all devices that support Google Play Services on
    // all API levels. It offers similar capabilities to PlatformStorage and is the
    // recommended solution for lower API levels on which PlatformStorage is not supported.
    implementation("androidx.appsearch:appsearch-play-services-storage:$appsearch_version")

}

Основные понятия AppSearch

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

Диаграмма, описывающая клиентское приложение и его взаимодействие со следующими концепциями AppSearch: база данных AppSearch, схема, типы схем, документы, сеанс и поиск. Рисунок 1. Схема концепций AppSearch: база данных AppSearch, схема, типы схем, документы, сеанс и поиск.

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

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

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

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

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

Документы

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

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

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

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

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

Хранилище платформы, локальное хранилище или хранилище сервисов Google Play

AppSearch предлагает три решения для хранения данных: LocalStorage, PlatformStorage и PlayServicesStorage. При использовании LocalStorage ваше приложение управляет индексом, который хранится в каталоге данных приложения. При использовании PlatformStorage и PlayServicesStorage ваше приложение вносит вклад в центральный индекс системы. Индекс PlatformStorage размещается на системном сервере, а индекс PlayServicesStorage – в хранилище сервисов Google Play. Доступ к данным в этих центральных индексах ограничен данными, которые предоставило ваше приложение, и данными, которыми с вами явно поделилось другое приложение. Все эти варианты хранения используют один и тот же API и могут быть взаимозаменяемыми в зависимости от версии устройства.

Обратите внимание, что поддержка функций зависит от выбранного хранилища и устройства. Подробнее о доступности функций…

Приведенный ниже код создает сеанс PlatformStorage на устройствах с Android 12 (уровень API 31) и более поздних версий, а также сеанс PlayServicesStorage или LocalStorage на устройствах с более ранними версиями ОС.

Kotlin

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    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 (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    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 проиндексированные данные можно показывать в интерфейсе системы. Приложения могут запретить показ некоторых или всех своих данных в системном интерфейсе. Подробнее об этом API рассказывается в документации по setSchemaTypeDisplayedBySystem().

Функции LocalStorage (совместимо с Android 5.0 и более поздними версиями) PlatformStorage (совместимо с Android 12 и более поздними версиями) PlayServicesStorage (совместимо с Android 5.0 и более поздними версиями)
Эффективный полнотекстовый поиск.
Поддержка нескольких языков
Уменьшенный размер двоичного файла.
Обмен данными между приложениями
Возможность показывать данные в интерфейсе системы.
Неограниченный размер и количество документов, которые можно индексировать.
Более быстрые операции без дополнительной задержки связывателя.

При выборе между LocalStorage и PlatformStorage нужно учитывать и другие факторы. Поскольку PlatformStorage оборачивает API Jetpack в системный сервис AppSearch, влияние на размер APK минимально по сравнению с использованием LocalStorage. Однако это также означает, что операции AppSearch вызывают дополнительную задержку связывателя при вызове системной службы AppSearch. В PlatformStorage AppSearch ограничивает количество и размер документов, которые приложение может индексировать, чтобы обеспечить эффективную работу центрального индекса. PlayServicesStorage имеет те же ограничения, что и PlatformStorage, и поддерживается только на устройствах с сервисами Google Play.

Доступность функций Google Pay по странам

Хотя все варианты хранилища используют один и тот же API, некоторые функции доступны не для всех вариантов хранилища и не на всех устройствах. В документации по API аннотация @RequiresFeature указывает на API, для которых требуется одна из этих функций, а класс Features определяет константу для каждой функции. Например, для INDEXING_TYPE_RANGE требуется Features.NUMERIC_SEARCH.

Для PlatformStorage поддержка функций зависит от версии Android на устройстве. Начиная с Android 13 (уровень API 33) поддержка функций также зависит от обновлений системы через Google Play, которые получило устройство. Например, PlatformStorage поддерживает числовой поиск (Features.NUMERIC_SEARCH), дословный поиск (Features.VERBATIM_SEARCH) и объединения (Features.JOIN_SPEC_AND_QUALIFIED_ID) только на следующих устройствах:

  • Устройства с Android 14 (уровень API 34) или более поздней версии.
  • Устройства с Android 13 (уровень API 33), на которых установлено необходимое обновление системы через Google Play.

Эти функции не поддерживаются на устройствах с Android 12 или Android 12L (уровни API 31 и 32).

Максимальное количество индексируемых свойств в документе также зависит от устройства и варианта хранения. Чтобы получить ограничение во время выполнения, вызовите getMaxIndexedProperties().

Не полагайтесь только на версию Android, чтобы определить, поддерживается ли функция. Прежде чем вызывать API, для которого требуется функция, убедитесь, что она поддерживается. Для этого вызовите isFeatureSupported() для объекта Features, который возвращает AppSearchSession.getFeatures(). Если обязательная функция не поддерживается, операция завершается сбоем и выдается исключение. Например, если схема включает свойство INDEXING_TYPE_RANGE, то на устройствах, которые не поддерживают Features.NUMERIC_SEARCH, произойдет сбой.

Следующий код проверяет, поддерживает ли открытый AppSearchSession числовой поиск:

Kotlin

val features = session.features
if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) {
    // Numeric search is supported. You can index properties with
    // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with
    // SearchSpec.Builder.setNumericSearchEnabled(true).
} else {
    // Numeric search isn't supported. Set a schema without range-indexed
    // properties, or switch to a different storage option.
}

Java

Features features = session.getFeatures();
if (features.isFeatureSupported(Features.NUMERIC_SEARCH)) {
    // Numeric search is supported. You can index properties with
    // LongPropertyConfig.INDEXING_TYPE_RANGE and enable numeric search with
    // SearchSpec.Builder.setNumericSearchEnabled(true).
} else {
    // Numeric search isn't supported. Set a schema without range-indexed
    // properties, or switch to a different storage option.
}

Начало работы с 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 с помощью конструктора класса документов 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, который предоставляет доступ к страницам объектов 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 (Kotlin). Приложение для создания заметок, которое использует AppSearch для индексирования заметок пользователя и позволяет выполнять поиск по ним.

Отправить отзыв

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

Система отслеживания ошибок

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