Создание соединителя содержимого

Коннектор контента — это программное обеспечение, которое обрабатывает данные в корпоративном репозитории и заполняет источник данных. Google предлагает следующие варианты разработки коннекторов контента:

Типичный коннектор контента выполняет следующие задачи:

  1. Считывает и обрабатывает параметры конфигурации.
  2. Извлекает из стороннего репозитория отдельные фрагменты индексируемых данных, называемые « элементами ».
  3. Объединяет списки контроля доступа (ACL), метаданные и данные контента в индексируемые элементы.
  4. Индексирует элементы в источнике данных Cloud Search.
  5. (Необязательно) Отслеживает уведомления об изменениях из репозитория. Уведомления об изменениях преобразуются в запросы на индексирование для синхронизации источника данных Cloud Search. Коннектор выполняет эту задачу только в том случае, если репозиторий поддерживает обнаружение изменений.

Создайте коннектор контента с помощью SDK коннектора контента.

В следующих разделах объясняется, как создать коннектор контента с помощью SDK для коннекторов контента.

Настройка зависимостей

Включите эти зависимости в свой файл сборки.

Мэйвен

xml <dependency> <groupId>com.google.enterprise.cloudsearch</groupId> <artifactId>google-cloudsearch-indexing-connector-sdk</artifactId> <version>v1-0.0.3</version> </dependency>

Грэдл

groovy compile group: 'com.google.enterprise.cloudsearch', name: 'google-cloudsearch-indexing-connector-sdk', version: 'v1-0.0.3'

Создайте конфигурацию коннектора.

Каждый коннектор использует конфигурационный файл для параметров, таких как идентификатор вашего репозитория. Определяйте параметры в виде пар ключ-значение , например, api.sourceId= 1234567890abcdef .

В состав Google Cloud Search SDK входят параметры, предоставляемые Google, для всех коннекторов. В файле конфигурации необходимо указать следующее:

  • Коннектор контента : Объявите api.sourceId и api.serviceAccountPrivateKeyFile . Они идентифицируют ваш репозиторий и закрытый ключ, необходимый для доступа.
  • Коннектор идентификации : укажите api.identitySourceId для идентификации внешнего источника идентификации. Для синхронизации пользователей также укажите api.customerId (уникальный идентификатор вашей учетной записи Google Workspace).

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

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

Передайте файл конфигурации коннектору.

Укажите системное свойство config , чтобы передать файл конфигурации. Используйте аргумент -D при запуске коннектора. Например:

java -classpath myconnector.jar -Dconfig=MyConfig.properties MyConnector

Если этот аргумент не будет указан, SDK попытается использовать файл с именем connector-config.properties расположенный в локальном каталоге.

Определите свою стратегию передвижения.

Основная функция коннектора контента — обход репозитория и индексирование его данных. Вам необходимо реализовать стратегию, основанную на размере и структуре вашего репозитория. Вы можете разработать собственную стратегию или выбрать стратегию из SDK:

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

SDK реализует эти стратегии в классах шаблонных коннекторов. Эти шаблоны могут ускорить разработку. Чтобы использовать шаблон, см. соответствующий раздел:

Создайте коннектор для полного обхода, используя шаблонный класс.

В этом разделе используется код из примера FullTraversalSample .

Реализуйте точку входа коннектора.

Точкой входа является метод main() . Он создает экземпляр Application и вызывает start() для запуска коннектора.

Перед вызовом application.start() используйте класс IndexingApplication.Builder для создания экземпляра шаблона FullTraversalConnector . Этот шаблон принимает объект Repository .

FullTraversalSample.java
/**
 * This sample connector uses the Cloud Search SDK template class for a full
 * traversal connector.
 *
 * @param args program command line arguments
 * @throws InterruptedException thrown if an abort is issued during initialization
 */
public static void main(String[] args) throws InterruptedException {
  Repository repository = new SampleRepository();
  IndexingConnector connector = new FullTraversalConnector(repository);
  IndexingApplication application = new IndexingApplication.Builder(connector, args).build();
  application.start();
}

SDK вызывает initConfig() после того, как ваш метод main() вызывает Application.build() . Метод initConfig() :

  1. Гарантирует, что Configuration еще не инициализирована.
  2. Инициализирует объект Configuration парами ключ-значение, предоставленными Google.

Реализуйте интерфейс репозитория.

Объект Repository осуществляет обход и индексирование элементов репозитория. При использовании шаблона достаточно переопределить только определенные методы в интерфейсе Repository . Для FullTraversalConnector переопределите:

  • init() : Для настройки и инициализации репозитория.
  • getAllDocs() : Для обхода и индексации всех элементов. Этот метод вызывается один раз для каждого запланированного обхода.
  • (Необязательно) getChanges() : Если ваш репозиторий поддерживает обнаружение изменений, переопределите этот метод для получения и индексации измененных элементов.
  • (Необязательно) close() : Для очистки репозитория во время завершения работы.

Каждый метод возвращает объект ApiOperation , который выполняет индексирование с помощью IndexingService.indexItem() .

Получить пользовательские параметры конфигурации

Для управления конфигурацией коннектора необходимо получить все пользовательские параметры из объекта Configuration . Выполните эту задачу в методе init() вашего класса Repository .

Класс Configuration содержит методы для получения данных различных типов. Каждый метод возвращает объект ConfigValue . Используйте метод get() объекта ConfigValue для получения значения. Этот фрагмент кода из FullTraversalSample показывает, как получить пользовательское целочисленное значение:

FullTraversalSample.java
@Override
public void init(RepositoryContext context) {
  log.info("Initializing repository");
  numberOfDocuments = Configuration.getInteger("sample.documentCount", 10).get();
}

Для получения и анализа параметров с несколькими значениями используйте один из парсеров типов класса Configuration . В этом фрагменте из коннектора из учебного пособия используется getMultiValue для получения списка имен репозиториев GitHub:

GithubRepository.java
ConfigValue<List<String>> repos = Configuration.getMultiValue(
    "github.repos",
    Collections.emptyList(),
    Configuration.STRING_PARSER);

Выполните полный обход

Переопределите getAllDocs() для выполнения полного обхода. Этот метод принимает контрольную точку для возобновления индексирования в случае его прерывания. Для каждого элемента:

  1. Установить права доступа.
  2. Установить метаданные.
  3. Объедините их в документ RepositoryDoc .
  4. Упакуйте каждый элемент в итератор, возвращаемый функцией getAllDocs() .

Если набор элементов слишком велик для одного вызова, используйте контрольную точку и вызовите метод hasMore(true) .

Установите права доступа для элемента.

В репозиториях используются списки контроля доступа (ACL) для идентификации пользователей или групп, имеющих доступ к элементу. В списке ACL перечислены идентификаторы авторизованных пользователей или групп.

Чтобы пользователи видели только те результаты поиска, к которым у них есть доступ, необходимо скопировать списки контроля доступа (ACL) вашего репозитория. Включите ACL при индексировании элемента, чтобы Google Cloud Search мог предоставить правильный уровень доступа.

SDK Content Connector включает классы и методы для моделирования списков контроля доступа (ACL) большинства репозиториев. Проанализируйте списки контроля доступа вашего репозитория и создайте соответствующие списки контроля доступа для Cloud Search во время индексирования. Моделирование сложных списков контроля доступа, например, использующих наследование, требует тщательного планирования. Для получения дополнительной информации см. раздел «Списки контроля доступа Cloud Search» .

Для установки доступа используйте класс Acl.Builder . Этот фрагмент из полного примера обхода позволяет всем пользователям домена ( getCustomerPrincipal() ) читать все элементы ( setReaders() ):

FullTraversalSample.java
// Make the document publicly readable within the domain
Acl acl = new Acl.Builder()
    .setReaders(Collections.singletonList(Acl.getCustomerPrincipal()))
    .build();

Для корректного моделирования списков контроля доступа (ACL) репозитория, особенно тех, которые используют модели наследования, необходима информация из списков контроля доступа Cloud Search .

Задайте метаданные для элемента

Метаданные хранятся в объекте Item . Для создания Item необходимы уникальный идентификатор, тип элемента, ACL, URL и версия. Используйте вспомогательный класс IndexingItemBuilder .

FullTraversalSample.java
// Url is required. Use google.com as a placeholder for this sample.
String viewUrl = "https://www.google.com";

// Version is required, set to current timestamp.
byte[] version = Longs.toByteArray(System.currentTimeMillis());

// Using the SDK item builder class to create the document with appropriate attributes
// (this can be expanded to include metadata fields etc.)
Item item = IndexingItemBuilder.fromConfiguration(Integer.toString(id))
    .setItemType(IndexingItemBuilder.ItemType.CONTENT_ITEM)
    .setAcl(acl)
    .setSourceRepositoryUrl(IndexingItemBuilder.FieldOrValue.withValue(viewUrl))
    .setVersion(version)
    .build();
Создайте индексируемый элемент

Используйте класс RepositoryDoc.Builder .

FullTraversalSample.java
// For this sample, content is just plain text
String content = String.format("Hello world from sample doc %d", id);
ByteArrayContent byteContent = ByteArrayContent.fromString("text/plain", content);

// Create the fully formed document
RepositoryDoc doc = new RepositoryDoc.Builder()
    .setItem(item)
    .setContent(byteContent, IndexingService.ContentFormat.TEXT)
    .build();

RepositoryDoc — это ApiOperation , выполняющий запрос IndexingService.indexItem() .

Используйте метод setRequestMode() класса RepositoryDoc.Builder , чтобы установить для запроса индексации режим ASYNCHRONOUS или SYNCHRONOUS :

ASYNCHRONOUS
Этот режим имеет большую задержку между индексированием и предоставлением доступа, но обеспечивает большую пропускную способность. Используйте асинхронный режим для первоначального индексирования (заполнения) всего репозитория.
SYNCHRONOUS
Этот режим имеет меньшую задержку между индексированием и предоставлением данных, но меньшую квоту пропускной способности. Используйте синхронный режим для индексирования обновлений и изменений в репозитории. Если режим запроса не указан, по умолчанию он устанавливается в режим SYNCHRONOUS .
Каждый индексируемый элемент упаковывается в итератор.

Метод getAllDocs() возвращает объект CheckpointCloseableIterable содержащий объекты RepositoryDoc . Используйте класс CheckpointCloseableIterableImpl.Builder .

FullTraversalSample.java
CheckpointCloseableIterable<ApiOperation> iterator =
  new CheckpointCloseableIterableImpl.Builder<>(allDocs).build();

Следующие шаги

Создайте коннектор для обхода списка, используя шаблонный класс.

Очередь индексирования Cloud Search содержит идентификаторы и необязательные хеши элементов репозитория. Коннектор обхода списка отправляет идентификаторы в эту очередь и извлекает их для индексирования. Cloud Search поддерживает эти очереди для определения статуса элементов, например, удалений. См. раздел «Очередь индексирования Cloud Search» .

В этом разделе рассматривается пример ListTraversalSample .

Реализуйте точку входа коннектора.

Метод main() создает экземпляр Application и вызывает метод start() . Используйте IndexingApplication.Builder для создания экземпляра шаблона ListingConnector .

ListTraversalSample.java
/**
 * This sample connector uses the Cloud Search SDK template class for a
 * list traversal connector.
 *
 * @param args program command line arguments
 * @throws InterruptedException thrown if an abort is issued during initialization
 */
public static void main(String[] args) throws InterruptedException {
  Repository repository = new SampleRepository();
  IndexingConnector connector = new ListingConnector(repository);
  IndexingApplication application = new IndexingApplication.Builder(connector, args).build();
  application.start();
}

Реализуйте интерфейс репозитория.

Переопределите следующие методы для ListingConnector :

  • init() : Для настройки репозитория.
  • getIds() : Для получения идентификаторов и хешей всех записей.
  • getDoc() : Для добавления, обновления или удаления элементов из индекса.
  • (Необязательно) getChanges() : Для инкрементальных обновлений с использованием обнаружения изменений.
  • (Необязательно) close() : Для очистки репозитория.

Выполните обход списка.

Переопределите getIds() для получения идентификаторов и хешей. Переопределите getDoc() для обработки каждого элемента в очереди индексирования Cloud Search.

Передайте идентификаторы элементов и хэш-значения.

Переопределите getIds() для получения идентификаторов и хешей контента. Упакуйте их в запрос PushItems в очередь индексирования.

ListTraversalSample.java
PushItems.Builder allIds = new PushItems.Builder();
for (Map.Entry<Integer, Long> entry : this.documents.entrySet()) {
  String documentId = Integer.toString(entry.getKey());
  String hash = this.calculateMetadataHash(entry.getKey());
  PushItem item = new PushItem().setMetadataHash(hash);
  log.info("Pushing " + documentId);
  allIds.addPushItem(documentId, item);
}

Используйте PushItems.Builder для упаковки идентификаторов и хешей.

ListTraversalSample.java
ApiOperation pushOperation = allIds.build();
CheckpointCloseableIterable<ApiOperation> iterator =
  new CheckpointCloseableIterableImpl.Builder<>(
      Collections.singletonList(pushOperation))
  .build();
return iterator;
Извлеките и обработайте каждый предмет.

Переопределите getDoc() для обработки элементов в очереди индексирования. Элементы могут быть новыми, измененными, неизмененными или удаленными.

  1. Проверьте, существует ли идентификатор элемента в репозитории. Если нет, удалите его.
  2. Проверьте статус индекса. Если статус не изменился ( ACCEPTED ), ничего не делайте.
  3. Индексируйте измененные или новые элементы: установите права доступа, задайте метаданные, объедините в RepositoryDoc и верните его.
Обработка удаленных элементов

Этот фрагмент кода показывает, как определить, существует ли элемент, и удалить его, если нет.

ListTraversalSample.java
String resourceName = item.getName();
int documentId = Integer.parseInt(resourceName);

if (!documents.containsKey(documentId)) {
  // Document no longer exists -- delete it
  log.info(() -> String.format("Deleting document %s", item.getName()));
  return ApiOperations.deleteItem(resourceName);
}
Обработка неизмененных элементов

Для обработки неизмененных элементов опрашивайте очередь индексирования.

ListTraversalSample.java
String currentHash = this.calculateMetadataHash(documentId);
if (this.canSkipIndexing(item, currentHash)) {
  // Document neither modified nor deleted, ack the push
  log.info(() -> String.format("Document %s not modified", item.getName()));
  PushItem pushItem = new PushItem().setType("NOT_MODIFIED");
  return new PushItems.Builder().addPushItem(resourceName, pushItem).build();
}

В примере для обнаружения изменений используется хеш.

ListTraversalSample.java
/**
 * Checks to see if an item is already up to date
 *
 * @param previousItem Polled item
 * @param currentHash  Metadata hash of the current github object
 * @return PushItem operation
 */
private boolean canSkipIndexing(Item previousItem, String currentHash) {
  if (previousItem.getStatus() == null || previousItem.getMetadata() == null) {
    return false;
  }
  String status = previousItem.getStatus().getCode();
  String previousHash = previousItem.getMetadata().getHash();
  return "ACCEPTED".equals(status)
      && previousHash != null
      && previousHash.equals(currentHash);
}
Установите права доступа для элемента.

В репозиториях используются списки контроля доступа (ACL) для идентификации пользователей или групп, имеющих доступ к элементу. В списке ACL перечислены идентификаторы авторизованных пользователей или групп.

Чтобы пользователи видели только те результаты поиска, к которым у них есть доступ, необходимо скопировать списки контроля доступа (ACL) вашего репозитория. Включите ACL при индексировании элемента, чтобы Google Cloud Search мог предоставить правильный уровень доступа.

SDK Content Connector включает классы и методы для моделирования списков контроля доступа (ACL) большинства репозиториев. Проанализируйте списки контроля доступа вашего репозитория и создайте соответствующие списки контроля доступа для Cloud Search во время индексирования. Моделирование сложных списков контроля доступа, например, использующих наследование, требует тщательного планирования. Для получения дополнительной информации см. раздел «Списки контроля доступа Cloud Search» .

Для установки доступа используйте класс Acl.Builder . Этот фрагмент из полного примера обхода позволяет всем пользователям домена ( getCustomerPrincipal() ) читать все элементы ( setReaders() ):

FullTraversalSample.java
// Make the document publicly readable within the domain
Acl acl = new Acl.Builder()
    .setReaders(Collections.singletonList(Acl.getCustomerPrincipal()))
    .build();

Для корректного моделирования списков контроля доступа (ACL) репозитория, особенно тех, которые используют модели наследования, необходима информация из списков контроля доступа Cloud Search .

Задайте метаданные для элемента
ListTraversalSample.java
// Url is required. Use google.com as a placeholder for this sample.
String viewUrl = "https://www.google.com";

// Version is required, set to current timestamp.
byte[] version = Longs.toByteArray(System.currentTimeMillis());

// Set metadata hash so queue can detect changes
String metadataHash = this.calculateMetadataHash(documentId);

// Using the SDK item builder class to create the document with
// appropriate attributes. This can be expanded to include metadata
// fields etc.
Item item = IndexingItemBuilder.fromConfiguration(Integer.toString(documentId))
    .setItemType(IndexingItemBuilder.ItemType.CONTENT_ITEM)
    .setAcl(acl)
    .setSourceRepositoryUrl(IndexingItemBuilder.FieldOrValue.withValue(viewUrl))
    .setVersion(version)
    .setHash(metadataHash)
    .build();
Создать индексируемый элемент
ListTraversalSample.java
// For this sample, content is just plain text
String content = String.format("Hello world from sample doc %d", documentId);
ByteArrayContent byteContent = ByteArrayContent.fromString("text/plain", content);

// Create the fully formed document
RepositoryDoc doc = new RepositoryDoc.Builder()
    .setItem(item)
    .setContent(byteContent, IndexingService.ContentFormat.TEXT)
    .build();

Используйте метод setRequestMode() класса RepositoryDoc.Builder , чтобы установить для запроса индексации режим ASYNCHRONOUS или SYNCHRONOUS :

ASYNCHRONOUS
Этот режим имеет большую задержку между индексированием и предоставлением доступа, но обеспечивает большую пропускную способность. Используйте асинхронный режим для первоначального индексирования (заполнения) всего репозитория.
SYNCHRONOUS
Этот режим имеет меньшую задержку между индексированием и предоставлением данных, но меньшую квоту пропускной способности. Используйте синхронный режим для индексирования обновлений и изменений в репозитории. Если режим запроса не указан, по умолчанию он устанавливается в режим SYNCHRONOUS .

Следующие шаги

Вот несколько дальнейших шагов, которые вы можете предпринять:

Создайте соединитель для обхода графа, используя шаблонный класс.

Очередь индексирования Cloud Search хранит идентификаторы и необязательные хэш-значения для каждого элемента в репозитории. Коннектор обхода графа отправляет идентификаторы элементов в очередь индексирования Google Cloud Search и извлекает их по одному для индексирования. Google Cloud Search поддерживает очереди и сравнивает содержимое очередей, чтобы определить статус элементов, например, был ли элемент удален из репозитория. Для получения дополнительной информации об очереди индексирования Cloud Search см. раздел «Очередь индексирования Google Cloud Search» .

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

Реализуйте точку входа коннектора.

Точкой входа в коннектор является метод main() . Этот метод создает экземпляр класса Application и вызывает его метод start() для запуска коннектора.

Перед вызовом application.start() используйте класс IndexingApplication.Builder для создания экземпляра шаблона ListingConnector . ListingConnector принимает объект Repository , методы которого вы можете реализовать.

Реализуйте интерфейс репозитория.

Переопределите init() , getIds() , getDoc() и, при необходимости, getChanges() или close() .

Выполните обход графа.

Переопределите getIds() для получения начальных идентификаторов и getDoc() для обработки элементов и добавления идентификаторов дочерних элементов в очередь.

Передайте идентификаторы элементов и хэш-значения.
GraphTraversalSample.java
PushItems.Builder allIds = new PushItems.Builder();
PushItem item = new PushItem();
allIds.addPushItem("root", item);
Извлеките и обработайте каждый предмет.
  1. Проверьте, существует ли идентификатор в репозитории. Если нет, удалите элемент.
  2. Для существующих элементов установите права доступа и метаданные, а затем объедините их в документ RepositoryDoc .
  3. Поместите идентификаторы дочерних элементов в очередь индексирования.
  4. Возвращает RepositoryDoc .
Обработка удаленных элементов
GraphTraversalSample.java
String resourceName = item.getName();
if (documentExists(resourceName)) {
  return buildDocumentAndChildren(resourceName);
}
// Document doesn't exist, delete it
log.info(() -> String.format("Deleting document %s", resourceName));
return ApiOperations.deleteItem(resourceName);
Задайте метаданные и создайте элемент.
GraphTraversalSample.java
// Url is required. Use google.com as a placeholder for this sample.
String viewUrl = "https://www.google.com";

// Version is required, set to current timestamp.
byte[] version = Longs.toByteArray(System.currentTimeMillis());

// Using the SDK item builder class to create the document with
// appropriate attributes. This can be expanded to include metadata
// fields etc.
Item item = IndexingItemBuilder.fromConfiguration(documentId)
    .setItemType(IndexingItemBuilder.ItemType.CONTENT_ITEM)
    .setAcl(acl)
    .setSourceRepositoryUrl(IndexingItemBuilder.FieldOrValue.withValue(viewUrl))
    .setVersion(version)
    .build();
GraphTraversalSample.java
// For this sample, content is just plain text
String content = String.format("Hello world from sample doc %s", documentId);
ByteArrayContent byteContent = ByteArrayContent.fromString("text/plain", content);

RepositoryDoc.Builder docBuilder = new RepositoryDoc.Builder()
    .setItem(item)
    .setContent(byteContent, IndexingService.ContentFormat.TEXT);
Поместите идентификаторы дочерних элементов в очередь индексирования.
GraphTraversalSample.java
// Queue the child nodes to visit after indexing this document
Set<String> childIds = getChildItemNames(documentId);
for (String id : childIds) {
  log.info(() -> String.format("Pushing child node %s", id));
  PushItem pushItem = new PushItem();
  docBuilder.addChildId(id, pushItem);
}

RepositoryDoc doc = docBuilder.build();

Создайте коннектор контента, используя REST API.

В следующих разделах объясняется, как создать коннектор контента с помощью REST API.

Определите свою стратегию передвижения.

Стратегии (полная, списочная и графовая) концептуально идентичны стратегиям SDK. Реализуйте выбранную вами стратегию, используя REST API.

Реализуйте свою стратегию обхода и проиндексируйте элементы.

Зарегистрируйте свою схему, затем заполните индекс, используя:

  1. (Необязательно) items.upload для файлов размером более 100 КиБ.
  2. (Необязательно) media.upload для загрузки медиафайлов.
  3. items.index используется для индексации элемента.

    Пример запроса на индексирование:

    {
      "name": "datasource/<data_source_id>/items/titanic",
      "acl": {
        "readers": [
          {
            "gsuitePrincipal": {
              "gsuiteDomain": true
            }
          }
        ]
      },
      "metadata": {
        "title": "Titanic",
        "viewUrl": "http://www.imdb.com/title/tt2234155/",
        "objectType": "movie"
      },
      "structuredData": {
        "object": {
          "properties": [
            {
              "name": "movieTitle",
              "textValues": { "values": ["Titanic"] }
            }
          ]
        }
      },
      "content": {
        "inlineContent": "A seventeen-year-old aristocrat falls in love...",
        "contentFormat": "TEXT"
      },
      "version": "01",
      "itemType": "CONTENT_ITEM"
    }
    
  4. (Необязательно) Используйте items.get для проверки индексации.

Обработка изменений в репозитории

Периодически выполняйте переиндексацию всего репозитория для полной индексации. Для обхода списков или графов используйте очередь индексации Google Cloud , чтобы отслеживать изменения и индексировать только то, что изменилось. Используйте items.push для добавления элементов в очередь.

,

Коннектор контента — это программное обеспечение, которое обрабатывает данные в корпоративном репозитории и заполняет источник данных. Google предлагает следующие варианты разработки коннекторов контента:

Типичный коннектор контента выполняет следующие задачи:

  1. Считывает и обрабатывает параметры конфигурации.
  2. Извлекает из стороннего репозитория отдельные фрагменты индексируемых данных, называемые « элементами ».
  3. Объединяет списки контроля доступа (ACL), метаданные и данные контента в индексируемые элементы.
  4. Индексирует элементы в источнике данных Cloud Search.
  5. (Необязательно) Отслеживает уведомления об изменениях из репозитория. Уведомления об изменениях преобразуются в запросы на индексирование для синхронизации источника данных Cloud Search. Коннектор выполняет эту задачу только в том случае, если репозиторий поддерживает обнаружение изменений.

Создайте коннектор контента с помощью SDK коннектора контента.

В следующих разделах объясняется, как создать коннектор контента с помощью SDK для коннекторов контента.

Настройка зависимостей

Включите эти зависимости в свой файл сборки.

Мэйвен

xml <dependency> <groupId>com.google.enterprise.cloudsearch</groupId> <artifactId>google-cloudsearch-indexing-connector-sdk</artifactId> <version>v1-0.0.3</version> </dependency>

Грэдл

groovy compile group: 'com.google.enterprise.cloudsearch', name: 'google-cloudsearch-indexing-connector-sdk', version: 'v1-0.0.3'

Создайте конфигурацию коннектора.

Каждый коннектор использует конфигурационный файл для параметров, таких как идентификатор вашего репозитория. Определяйте параметры в виде пар ключ-значение , например, api.sourceId= 1234567890abcdef .

В состав Google Cloud Search SDK входят параметры, предоставляемые Google, для всех коннекторов. В файле конфигурации необходимо указать следующее:

  • Коннектор контента : Объявите api.sourceId и api.serviceAccountPrivateKeyFile . Они идентифицируют ваш репозиторий и закрытый ключ, необходимый для доступа.
  • Коннектор идентификации : укажите api.identitySourceId для идентификации внешнего источника идентификации. Для синхронизации пользователей также укажите api.customerId (уникальный идентификатор вашей учетной записи Google Workspace).

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

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

Передайте файл конфигурации коннектору.

Укажите системное свойство config , чтобы передать файл конфигурации. Используйте аргумент -D при запуске коннектора. Например:

java -classpath myconnector.jar -Dconfig=MyConfig.properties MyConnector

Если этот аргумент не будет указан, SDK попытается использовать файл с именем connector-config.properties расположенный в локальном каталоге.

Определите свою стратегию передвижения.

Основная функция коннектора контента — обход репозитория и индексирование его данных. Вам необходимо реализовать стратегию, основанную на размере и структуре вашего репозитория. Вы можете разработать собственную стратегию или выбрать стратегию из SDK:

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

SDK реализует эти стратегии в классах шаблонных коннекторов. Эти шаблоны могут ускорить разработку. Чтобы использовать шаблон, см. соответствующий раздел:

Создайте коннектор для полного обхода, используя шаблонный класс.

В этом разделе используется код из примера FullTraversalSample .

Реализуйте точку входа коннектора.

Точкой входа является метод main() . Он создает экземпляр Application и вызывает start() для запуска коннектора.

Перед вызовом application.start() используйте класс IndexingApplication.Builder для создания экземпляра шаблона FullTraversalConnector . Этот шаблон принимает объект Repository .

FullTraversalSample.java
/**
 * This sample connector uses the Cloud Search SDK template class for a full
 * traversal connector.
 *
 * @param args program command line arguments
 * @throws InterruptedException thrown if an abort is issued during initialization
 */
public static void main(String[] args) throws InterruptedException {
  Repository repository = new SampleRepository();
  IndexingConnector connector = new FullTraversalConnector(repository);
  IndexingApplication application = new IndexingApplication.Builder(connector, args).build();
  application.start();
}

SDK вызывает initConfig() после того, как ваш метод main() вызывает Application.build() . Метод initConfig() :

  1. Гарантирует, что Configuration еще не инициализирована.
  2. Инициализирует объект Configuration парами ключ-значение, предоставленными Google.

Реализуйте интерфейс репозитория.

Объект Repository осуществляет обход и индексирование элементов репозитория. При использовании шаблона достаточно переопределить только определенные методы в интерфейсе Repository . Для FullTraversalConnector переопределите:

  • init() : Для настройки и инициализации репозитория.
  • getAllDocs() : Для обхода и индексации всех элементов. Этот метод вызывается один раз для каждого запланированного обхода.
  • (Необязательно) getChanges() : Если ваш репозиторий поддерживает обнаружение изменений, переопределите этот метод для получения и индексации измененных элементов.
  • (Необязательно) close() : Для очистки репозитория во время завершения работы.

Каждый метод возвращает объект ApiOperation , который выполняет индексирование с помощью IndexingService.indexItem() .

Получить пользовательские параметры конфигурации

Для управления конфигурацией коннектора необходимо получить все пользовательские параметры из объекта Configuration . Выполните эту задачу в методе init() вашего класса Repository .

Класс Configuration содержит методы для получения данных различных типов. Каждый метод возвращает объект ConfigValue . Используйте метод get() объекта ConfigValue для получения значения. Этот фрагмент кода из FullTraversalSample показывает, как получить пользовательское целочисленное значение:

FullTraversalSample.java
@Override
public void init(RepositoryContext context) {
  log.info("Initializing repository");
  numberOfDocuments = Configuration.getInteger("sample.documentCount", 10).get();
}

Для получения и анализа параметров с несколькими значениями используйте один из парсеров типов класса Configuration . В этом фрагменте из коннектора из учебного пособия используется getMultiValue для получения списка имен репозиториев GitHub:

GithubRepository.java
ConfigValue<List<String>> repos = Configuration.getMultiValue(
    "github.repos",
    Collections.emptyList(),
    Configuration.STRING_PARSER);

Выполните полный обход

Переопределите getAllDocs() для выполнения полного обхода. Этот метод принимает контрольную точку для возобновления индексирования в случае его прерывания. Для каждого элемента:

  1. Установить права доступа.
  2. Установить метаданные.
  3. Объедините их в документ RepositoryDoc .
  4. Упакуйте каждый элемент в итератор, возвращаемый функцией getAllDocs() .

Если набор элементов слишком велик для одного вызова, используйте контрольную точку и вызовите метод hasMore(true) .

Установите права доступа для элемента.

В репозиториях используются списки контроля доступа (ACL) для идентификации пользователей или групп, имеющих доступ к элементу. В списке ACL перечислены идентификаторы авторизованных пользователей или групп.

Чтобы пользователи видели только те результаты поиска, к которым у них есть доступ, необходимо скопировать списки контроля доступа (ACL) вашего репозитория. Включите ACL при индексировании элемента, чтобы Google Cloud Search мог предоставить правильный уровень доступа.

SDK Content Connector включает классы и методы для моделирования списков контроля доступа (ACL) большинства репозиториев. Проанализируйте списки контроля доступа вашего репозитория и создайте соответствующие списки контроля доступа для Cloud Search во время индексирования. Моделирование сложных списков контроля доступа, например, использующих наследование, требует тщательного планирования. Для получения дополнительной информации см. раздел «Списки контроля доступа Cloud Search» .

Для установки доступа используйте класс Acl.Builder . Этот фрагмент из полного примера обхода позволяет всем пользователям домена ( getCustomerPrincipal() ) читать все элементы ( setReaders() ):

FullTraversalSample.java
// Make the document publicly readable within the domain
Acl acl = new Acl.Builder()
    .setReaders(Collections.singletonList(Acl.getCustomerPrincipal()))
    .build();

Для корректного моделирования списков контроля доступа (ACL) репозитория, особенно тех, которые используют модели наследования, необходима информация из списков контроля доступа Cloud Search .

Задайте метаданные для элемента

Метаданные хранятся в объекте Item . Для создания Item необходимы уникальный идентификатор, тип элемента, ACL, URL и версия. Используйте вспомогательный класс IndexingItemBuilder .

FullTraversalSample.java
// Url is required. Use google.com as a placeholder for this sample.
String viewUrl = "https://www.google.com";

// Version is required, set to current timestamp.
byte[] version = Longs.toByteArray(System.currentTimeMillis());

// Using the SDK item builder class to create the document with appropriate attributes
// (this can be expanded to include metadata fields etc.)
Item item = IndexingItemBuilder.fromConfiguration(Integer.toString(id))
    .setItemType(IndexingItemBuilder.ItemType.CONTENT_ITEM)
    .setAcl(acl)
    .setSourceRepositoryUrl(IndexingItemBuilder.FieldOrValue.withValue(viewUrl))
    .setVersion(version)
    .build();
Создайте индексируемый элемент

Используйте класс RepositoryDoc.Builder .

FullTraversalSample.java
// For this sample, content is just plain text
String content = String.format("Hello world from sample doc %d", id);
ByteArrayContent byteContent = ByteArrayContent.fromString("text/plain", content);

// Create the fully formed document
RepositoryDoc doc = new RepositoryDoc.Builder()
    .setItem(item)
    .setContent(byteContent, IndexingService.ContentFormat.TEXT)
    .build();

RepositoryDoc — это ApiOperation , выполняющий запрос IndexingService.indexItem() .

Используйте метод setRequestMode() класса RepositoryDoc.Builder , чтобы установить для запроса индексации режим ASYNCHRONOUS или SYNCHRONOUS :

ASYNCHRONOUS
Этот режим имеет большую задержку между индексированием и предоставлением доступа, но обеспечивает большую пропускную способность. Используйте асинхронный режим для первоначального индексирования (заполнения) всего репозитория.
SYNCHRONOUS
Этот режим имеет меньшую задержку между индексированием и предоставлением данных, но меньшую квоту пропускной способности. Используйте синхронный режим для индексирования обновлений и изменений в репозитории. Если режим запроса не указан, по умолчанию он устанавливается в режим SYNCHRONOUS .
Каждый индексируемый элемент упаковывается в итератор.

Метод getAllDocs() возвращает объект CheckpointCloseableIterable содержащий объекты RepositoryDoc . Используйте класс CheckpointCloseableIterableImpl.Builder .

FullTraversalSample.java
CheckpointCloseableIterable<ApiOperation> iterator =
  new CheckpointCloseableIterableImpl.Builder<>(allDocs).build();

Следующие шаги

Создайте коннектор для обхода списка, используя шаблонный класс.

Очередь индексирования Cloud Search содержит идентификаторы и необязательные хеши элементов репозитория. Коннектор обхода списка отправляет идентификаторы в эту очередь и извлекает их для индексирования. Cloud Search поддерживает эти очереди для определения статуса элементов, например, удалений. См. раздел «Очередь индексирования Cloud Search» .

В этом разделе рассматривается пример ListTraversalSample .

Реализуйте точку входа коннектора.

Метод main() создает экземпляр Application и вызывает метод start() . Используйте IndexingApplication.Builder для создания экземпляра шаблона ListingConnector .

ListTraversalSample.java
/**
 * This sample connector uses the Cloud Search SDK template class for a
 * list traversal connector.
 *
 * @param args program command line arguments
 * @throws InterruptedException thrown if an abort is issued during initialization
 */
public static void main(String[] args) throws InterruptedException {
  Repository repository = new SampleRepository();
  IndexingConnector connector = new ListingConnector(repository);
  IndexingApplication application = new IndexingApplication.Builder(connector, args).build();
  application.start();
}

Реализуйте интерфейс репозитория.

Переопределите следующие методы для ListingConnector :

  • init() : Для настройки репозитория.
  • getIds() : Для получения идентификаторов и хешей всех записей.
  • getDoc() : Для добавления, обновления или удаления элементов из индекса.
  • (Необязательно) getChanges() : Для инкрементальных обновлений с использованием обнаружения изменений.
  • (Необязательно) close() : Для очистки репозитория.

Выполните обход списка.

Переопределите getIds() для получения идентификаторов и хешей. Переопределите getDoc() для обработки каждого элемента в очереди индексирования Cloud Search.

Передайте идентификаторы элементов и хэш-значения.

Переопределите getIds() для получения идентификаторов и хешей контента. Упакуйте их в запрос PushItems в очередь индексирования.

ListTraversalSample.java
PushItems.Builder allIds = new PushItems.Builder();
for (Map.Entry<Integer, Long> entry : this.documents.entrySet()) {
  String documentId = Integer.toString(entry.getKey());
  String hash = this.calculateMetadataHash(entry.getKey());
  PushItem item = new PushItem().setMetadataHash(hash);
  log.info("Pushing " + documentId);
  allIds.addPushItem(documentId, item);
}

Используйте PushItems.Builder для упаковки идентификаторов и хешей.

ListTraversalSample.java
ApiOperation pushOperation = allIds.build();
CheckpointCloseableIterable<ApiOperation> iterator =
  new CheckpointCloseableIterableImpl.Builder<>(
      Collections.singletonList(pushOperation))
  .build();
return iterator;
Извлеките и обработайте каждый предмет.

Переопределите getDoc() для обработки элементов в очереди индексирования. Элементы могут быть новыми, измененными, неизмененными или удаленными.

  1. Проверьте, существует ли идентификатор элемента в репозитории. Если нет, удалите его.
  2. Проверьте статус индекса. Если статус не изменился ( ACCEPTED ), ничего не делайте.
  3. Индексируйте измененные или новые элементы: установите права доступа, задайте метаданные, объедините в RepositoryDoc и верните его.
Обработка удаленных элементов

Этот фрагмент кода показывает, как определить, существует ли элемент, и удалить его, если нет.

ListTraversalSample.java
String resourceName = item.getName();
int documentId = Integer.parseInt(resourceName);

if (!documents.containsKey(documentId)) {
  // Document no longer exists -- delete it
  log.info(() -> String.format("Deleting document %s", item.getName()));
  return ApiOperations.deleteItem(resourceName);
}
Обработка неизмененных элементов

Для обработки неизмененных элементов опрашивайте очередь индексирования.

ListTraversalSample.java
String currentHash = this.calculateMetadataHash(documentId);
if (this.canSkipIndexing(item, currentHash)) {
  // Document neither modified nor deleted, ack the push
  log.info(() -> String.format("Document %s not modified", item.getName()));
  PushItem pushItem = new PushItem().setType("NOT_MODIFIED");
  return new PushItems.Builder().addPushItem(resourceName, pushItem).build();
}

В примере для обнаружения изменений используется хеш.

ListTraversalSample.java
/**
 * Checks to see if an item is already up to date
 *
 * @param previousItem Polled item
 * @param currentHash  Metadata hash of the current github object
 * @return PushItem operation
 */
private boolean canSkipIndexing(Item previousItem, String currentHash) {
  if (previousItem.getStatus() == null || previousItem.getMetadata() == null) {
    return false;
  }
  String status = previousItem.getStatus().getCode();
  String previousHash = previousItem.getMetadata().getHash();
  return "ACCEPTED".equals(status)
      && previousHash != null
      && previousHash.equals(currentHash);
}
Установите права доступа для элемента.

В репозиториях используются списки контроля доступа (ACL) для идентификации пользователей или групп, имеющих доступ к элементу. В списке ACL перечислены идентификаторы авторизованных пользователей или групп.

Чтобы пользователи видели только те результаты поиска, к которым у них есть доступ, необходимо скопировать списки контроля доступа (ACL) вашего репозитория. Включите ACL при индексировании элемента, чтобы Google Cloud Search мог предоставить правильный уровень доступа.

SDK Content Connector включает классы и методы для моделирования списков контроля доступа (ACL) большинства репозиториев. Проанализируйте списки контроля доступа вашего репозитория и создайте соответствующие списки контроля доступа для Cloud Search во время индексирования. Моделирование сложных списков контроля доступа, например, использующих наследование, требует тщательного планирования. Для получения дополнительной информации см. раздел «Списки контроля доступа Cloud Search» .

Для установки доступа используйте класс Acl.Builder . Этот фрагмент из полного примера обхода позволяет всем пользователям домена ( getCustomerPrincipal() ) читать все элементы ( setReaders() ):

FullTraversalSample.java
// Make the document publicly readable within the domain
Acl acl = new Acl.Builder()
    .setReaders(Collections.singletonList(Acl.getCustomerPrincipal()))
    .build();

Для корректного моделирования списков контроля доступа (ACL) репозитория, особенно тех, которые используют модели наследования, необходима информация из списков контроля доступа Cloud Search .

Задайте метаданные для элемента
ListTraversalSample.java
// Url is required. Use google.com as a placeholder for this sample.
String viewUrl = "https://www.google.com";

// Version is required, set to current timestamp.
byte[] version = Longs.toByteArray(System.currentTimeMillis());

// Set metadata hash so queue can detect changes
String metadataHash = this.calculateMetadataHash(documentId);

// Using the SDK item builder class to create the document with
// appropriate attributes. This can be expanded to include metadata
// fields etc.
Item item = IndexingItemBuilder.fromConfiguration(Integer.toString(documentId))
    .setItemType(IndexingItemBuilder.ItemType.CONTENT_ITEM)
    .setAcl(acl)
    .setSourceRepositoryUrl(IndexingItemBuilder.FieldOrValue.withValue(viewUrl))
    .setVersion(version)
    .setHash(metadataHash)
    .build();
Создать индексируемый элемент
ListTraversalSample.java
// For this sample, content is just plain text
String content = String.format("Hello world from sample doc %d", documentId);
ByteArrayContent byteContent = ByteArrayContent.fromString("text/plain", content);

// Create the fully formed document
RepositoryDoc doc = new RepositoryDoc.Builder()
    .setItem(item)
    .setContent(byteContent, IndexingService.ContentFormat.TEXT)
    .build();

Используйте метод setRequestMode() класса RepositoryDoc.Builder , чтобы установить для запроса индексации режим ASYNCHRONOUS или SYNCHRONOUS :

ASYNCHRONOUS
Этот режим имеет большую задержку между индексированием и предоставлением доступа, но обеспечивает большую пропускную способность. Используйте асинхронный режим для первоначального индексирования (заполнения) всего репозитория.
SYNCHRONOUS
Этот режим имеет меньшую задержку между индексированием и предоставлением данных, но меньшую квоту пропускной способности. Используйте синхронный режим для индексирования обновлений и изменений в репозитории. Если режим запроса не указан, по умолчанию он устанавливается в режим SYNCHRONOUS .

Следующие шаги

Вот несколько дальнейших шагов, которые вы можете предпринять:

Создайте соединитель для обхода графа, используя шаблонный класс.

Очередь индексирования Cloud Search хранит идентификаторы и необязательные хэш-значения для каждого элемента в репозитории. Коннектор обхода графа отправляет идентификаторы элементов в очередь индексирования Google Cloud Search и извлекает их по одному для индексирования. Google Cloud Search поддерживает очереди и сравнивает содержимое очередей, чтобы определить статус элементов, например, был ли элемент удален из репозитория. Для получения дополнительной информации об очереди индексирования Cloud Search см. раздел «Очередь индексирования Google Cloud Search» .

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

Реализуйте точку входа коннектора.

Точкой входа в коннектор является метод main() . Этот метод создает экземпляр класса Application и вызывает его метод start() для запуска коннектора.

Before calling application.start() , use the IndexingApplication.Builder class to instantiate the ListingConnector template. The ListingConnector accepts a Repository object whose methods you implement.

Implement the Repository interface

Override init() , getIds() , getDoc() , and optionally getChanges() or close() .

Perform the graph traversal

Override getIds() to retrieve initial IDs and getDoc() to handle items and push child IDs to the queue.

Push item IDs and hash values
GraphTraversalSample.java
PushItems.Builder allIds = new PushItems.Builder();
PushItem item = new PushItem();
allIds.addPushItem("root", item);
Retrieve and handle each item
  1. Check if the ID exists in the repository. If not, delete the item.
  2. For existing items, set permissions and metadata, and combine them into a RepositoryDoc .
  3. Push child IDs to the Indexing Queue.
  4. Return the RepositoryDoc .
Handle deleted items
GraphTraversalSample.java
String resourceName = item.getName();
if (documentExists(resourceName)) {
  return buildDocumentAndChildren(resourceName);
}
// Document doesn't exist, delete it
log.info(() -> String.format("Deleting document %s", resourceName));
return ApiOperations.deleteItem(resourceName);
Set metadata and create the item
GraphTraversalSample.java
// Url is required. Use google.com as a placeholder for this sample.
String viewUrl = "https://www.google.com";

// Version is required, set to current timestamp.
byte[] version = Longs.toByteArray(System.currentTimeMillis());

// Using the SDK item builder class to create the document with
// appropriate attributes. This can be expanded to include metadata
// fields etc.
Item item = IndexingItemBuilder.fromConfiguration(documentId)
    .setItemType(IndexingItemBuilder.ItemType.CONTENT_ITEM)
    .setAcl(acl)
    .setSourceRepositoryUrl(IndexingItemBuilder.FieldOrValue.withValue(viewUrl))
    .setVersion(version)
    .build();
GraphTraversalSample.java
// For this sample, content is just plain text
String content = String.format("Hello world from sample doc %s", documentId);
ByteArrayContent byteContent = ByteArrayContent.fromString("text/plain", content);

RepositoryDoc.Builder docBuilder = new RepositoryDoc.Builder()
    .setItem(item)
    .setContent(byteContent, IndexingService.ContentFormat.TEXT);
Place child IDs in the Indexing Queue
GraphTraversalSample.java
// Queue the child nodes to visit after indexing this document
Set<String> childIds = getChildItemNames(documentId);
for (String id : childIds) {
  log.info(() -> String.format("Pushing child node %s", id));
  PushItem pushItem = new PushItem();
  docBuilder.addChildId(id, pushItem);
}

RepositoryDoc doc = docBuilder.build();

Create a content connector using the REST API

The following sections explain how to create a content connector using the REST API.

Determine your traversal strategy

The strategies (Full, List, and Graph) are conceptually the same as for the SDK. Implement your chosen strategy using the REST API.

Implement your traversal strategy and index items

Register your schema, then populate the index using:

  1. (Optional) items.upload for files larger than 100 KiB.
  2. (Optional) media.upload for media files.
  3. items.index to index the item.

    Example indexing request:

    {
      "name": "datasource/<data_source_id>/items/titanic",
      "acl": {
        "readers": [
          {
            "gsuitePrincipal": {
              "gsuiteDomain": true
            }
          }
        ]
      },
      "metadata": {
        "title": "Titanic",
        "viewUrl": "http://www.imdb.com/title/tt2234155/",
        "objectType": "movie"
      },
      "structuredData": {
        "object": {
          "properties": [
            {
              "name": "movieTitle",
              "textValues": { "values": ["Titanic"] }
            }
          ]
        }
      },
      "content": {
        "inlineContent": "A seventeen-year-old aristocrat falls in love...",
        "contentFormat": "TEXT"
      },
      "version": "01",
      "itemType": "CONTENT_ITEM"
    }
    
  4. (Optional) Use items.get to verify indexing.

Handle repository changes

Periodically reindex the entire repository for full indexing. For list or graph traversal, use the Google Cloud Indexing Queue to track changes and only index what has changed. Use items.push to add items to the queue.