В этом руководстве мы узнаем, как реализовать поиск GraphQL в приложении React с помощью AWS AppSync и React Apollo.

Окончательный код этого руководства находится здесь.

Для этого у нас будет поле поиска, которое запускает поиск, когда пользователь вводит текст в поле ввода, и обновляет пользовательский интерфейс, когда результаты возвращаются из нашего GraphQL API.

Начнем с создания нового GraphQL API с помощью AWS AppSync. Затем мы продемонстрируем, как использовать встроенный редактор запросов, чтобы добавлять элементы в источник данных и запрашивать их из консоли AWS AppSync.

Затем мы подключим API к нашему приложению React и реализуем базовое поле поиска, которое будет выполнять поиск в поле описания нашего источника данных.

Наконец, мы обновим API, чтобы добавить новую searchField строку в нашу таблицу, которая позволит нам запрашивать все поля из одного запроса, объединив другие поля в этот всеобъемлющий searchField. Мы обновим функцию поиска, чтобы не учитывать регистр.

Есть несколько способов реализовать поиск с помощью AWS AppSync. В этом руководстве мы будем использовать фильтры DynamoDB, которые автоматически создаются для нас при создании AppSync API. Для более сложных случаев использования вы также можете воспользоваться Amazon Elasticsearch в качестве источника данных AWS AppSync. Я опубликую продолжение этой статьи, часть 2, где в ближайшем будущем покажу, как реализовать более сложные поисковые операции, такие как нечеткий текст, время и геопространственные запросы.

Начиная

Первое, что нам нужно сделать, это создать новый AppSync API. Для этого перейдите в панель управления AWS AppSync и нажмите Создать API.

Выберите Автор с нуля, дайте API имя и нажмите Создать:

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

Теперь мы определим новый тип в разделе Определить или выбрать тип на панели инструментов под названием IceCream со следующими свойствами:

type IceCream {
 id: ID!
 name: String!
 description: String!
 rating: Float
}

Затем прокрутите вниз и нажмите Создать:

Теперь, когда этот тип создан вместе с его ресурсами, мы можем приступить к выполнению операций с API. Во-первых, давайте создадим мутацию, добавив несколько разных видов мороженого. Для этого нажмите Запросы в меню слева и выполните несколько изменений:

mutation create {
  createIceCream(input: {
    name: "Peanut Butter World"
    description: "Milk chocolate ice cream with peanut buttery swirls & chocolate cookie swirls"
  }) {
    name
  }
}

Выполните эту мутацию несколько раз с разными данными, чтобы заполнить наш API парой видов мороженого.

Теперь в нашей базе данных есть информация, и мы можем начать запрос данных, которые нам нужны!

Во-первых, давайте попробуем, запросив список всех видов мороженого в нашей базе данных. Для этого в том же редакторе запросов выполните следующий запрос:

query listAll {
  listIceCreams {
    items {
      name
      description
    }
  }
}

Это должно дать вам список всех мороженых в вашей базе данных. Затем давайте попробуем отфильтровать по описанию только мороженое с шоколадом в нем. Вот как мы реализуем поиск на клиенте с динамическим значением, а пока давайте просто используем жестко запрограммированное значение:

query listAll {
  listIceCreams(filter: {
    description: {
      contains: "chocolate"
    }
  }) {
    items {
      name
      description
    }
  }
}

Как работает фильтрация

Если вы посмотрите на схему и просмотрите запрос listIceCreams, вы увидите, что есть три необязательных аргумента: filter, limit, & nextToken.

filter - это тип TableIceCreamFilterInput, который позволяет нам фильтровать по любому значению в поле.

Давайте посмотрим на ввод TableStringFilterInput:

description - это тип ввода TableStringFilterInput:

Здесь, в TableStringFilterInput, мы можем увидеть все способы фильтрации. Мы используем фильтр содержит. По сути, мы говорим, что если поле содержит наш поисковый запрос, вернуть результат. Вы также можете видеть, что у нас есть варианты равно (eq), не равно (ne), notContains и многие другие.

Подключение клиентского приложения React

Теперь давайте подключим это к приложению React.

Во-первых, нам нужно создать новое приложение React и установить несколько зависимостей. Мы будем использовать Create React App CLI:

create-react-app appsync-search

Затем перейдите в новый каталог и установите следующие зависимости:

cd appsync-search
yarn add aws-appsync aws-appsync-react react-apollo graphql-tag lodash

Мы будем использовать пакеты AppSync и Apollo для взаимодействия с нашим API, а также пакет lodash для устранения ошибок при поиске позже.

Затем давайте загрузим файл AppSync.js из панели управления AppSync и сохраним его в каталоге src нашего нового проекта:

Затем обновим index.js:

Все, что мы здесь сделали, это создали новый клиент AppSync, используя комбинацию нашей AppSync.js конфигурации вместе с AWS AppSync и React Apollo.

Реализация функции поиска на клиенте

Для реализации поиска через клиент воспользуемся функцией data.fetchMore из React Apollo. Мы передадим нашему компоненту onSearch функцию, которая будет передавать поисковый запрос. onSearch вызовет props.data.fetchMore, передавая query, variables, & updateQuery, необходимые для выполнения операции.

Запрос GraphlQL, включая функцию onSearch, будет выглядеть так:

export default compose(
  graphql(ListIceCreams, {
    options: data => ({
      fetchPolicy: 'cache-and-network'
    }),
    props: props => ({
      onSearch: searchQuery => {
        return props.data.fetchMore({
          query: searchQuery === '' ? ListIceCreams : SearchIceCreams,
          variables: {
            searchQuery
          },
          updateQuery: (previousResult, { fetchMoreResult }) => ({
            ...previousResult,
            listIceCreams: {
              ...previousResult.listIceCreams,
              items: fetchMoreResult.listIceCreams.items
            }
          })
        })
      },
      data: props.data
    })
  })
)(App);

Чтобы инициировать запрос, мы будем использовать debounce from lodash, чтобы запускать функцию только в том случае, если пользователь не вводит текст в течение определенного периода, в нашем случае 250 миллисекунд:

onChange = (e) => {
  const value = e.target.value
  this.handleFilter(value)
}
handleFilter = debounce((val) => {
  this.props.onSearch(val)
}, 250)

Окончательный код, включая стили, находится здесь:

  1. Импортировать debounce из lodash
  2. Мы создаем поисковый запрос, ожидая аргумента searchQuery, который будет нашим запросом.
  3. У нас также есть обычный запрос для получения всех элементов (если поиск - пустая строка), а также для первоначальной загрузки приложения со всеми данными.
  4. Мы создаем начальное состояние, устанавливая searchQuery на пустую строку.
  5. onChange не будет вызывать саму функцию поиска, вместо этого он будет вызывать функцию handleFilter, передавая значение ввода в качестве аргумента.
  6. handleFilter вызовет функцию onSearch, переданную нашему компоненту в качестве свойств, передавая значение поиска. Мы используем debounce для выполнения операции только в том случае, если пользователь не печатал в течение последних 250 миллисекунд.
  7. Если приложение загружает данные из поиска, мы показываем пользователю сообщение Поиск.
  8. Если результатов поиска нет, мы отображаем сообщение, информирующее пользователя об отсутствии результатов.
  9. Мы сопоставляем элементы, возвращаемые API, отображая как имя, так и описание.
  10. Проверяем свойство searchQuery. Если это пустая строка, мы выполняем запрос для получения всех результатов (ListIceCreams), если searchQuery не является пустой строкой, мы выполняем поиск (SearchIceCreams).
  11. В функции updateQuery мы обновляем кеш, добавляя новые результаты из нашего поискового запроса (fetchMoreResult.listIceCreams.items), вызывая повторный рендеринг нашего компонента и показывая их пользователю.

Теперь у нас должна быть возможность запускать приложение и выполнять поиск в поле описания нашей таблицы!

npm start

Улучшения

То, что у нас есть сейчас, - отличное начало, но в идеале мы хотели бы иметь возможность искать по всем полям в таблице, а не только по описанию.

Мы также хотели бы сделать поиск без учета регистра и учета регистра. Давайте обновим наш API и клиентское приложение, чтобы сделать и то, и другое!

Обновление API AWS AppSync

Следующее, что мы сделаем, чтобы это заработало, - это обновить API, чтобы он мог выполнять поиск по всем полям, а не по одному.

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

Чтобы это заработало, нам нужно обновить преобразователь для создания нового элемента. Этот преобразователь является преобразователем createIceCream. Чтобы обновить этот преобразователь, откройте представление схемы в консоли AWS AppSync, а затем щелкните преобразователь рядом с полем:

В этом представлении мы обновим шаблон сопоставления запроса, чтобы он выглядел следующим образом:

Что тут происходит?

Сначала мы создаем новую переменную с именем vars, которая содержит значения, переданные в результате мутации.
Затем мы создаем две новые переменные (s1 & s2), которые принимают значения name и description и сохраняют их в нижнем регистре.
Наконец, мы добавляем новую пару ключ / значение к нашей vars карте, содержащей новые поля, и называем это значение searchField.

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

Затем нам нужно обновить вход TableIceCreamFilterInput в нашей схеме, чтобы добавить новое свойство searchField:

input TableIceCreamFilterInput {
 id: TableIDFilterInput
 name: TableStringFilterInput
 description: TableStringFilterInput
 rating: TableFloatFilterInput
 searchField: TableStringFilterInput
}

Обновите вход TableIceCreamFilterInput на указанное выше и нажмите Сохранить схему, чтобы обновить схему.

Поскольку у наших старых свойств не было свойства searchField, мы не можем запрашивать их. Давайте удалим все, что есть в нашей базе данных, и начнем с новой настройки.

Чтобы удалить эти элементы, нажмите Источники данных в левом меню, нажмите на источник данных IceCreamTable, выберите вкладку Элементы в DynamoDB и удалите все из предметов:

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

mutation create {
  createIceCream(input: {
    name: "Peanut Butter World"
    description: "Milk chocolate ice cream with peanut buttery swirls & chocolate cookie swirls"
  }) {
    name
  }
}

Опять же, создайте несколько разных видов мороженого.

Теперь снова щелкните Источники данных, откройте таблицу DynamoDB и просмотрите таблицу. Теперь вы должны увидеть новое поле searchField в своей таблице:

А теперь давайте попробуем выполнить запросы на основе этого нового поля searchField.

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

query listAll {
  listIceCreams(filter: {
    searchField: {
      contains: "s'mores"
    }
  }) {
    items {
      name
      description
    }
  }
}

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

Обновление клиента

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

Во-первых, нам нужно обновить определение нашего SearchIceCreams запроса, чтобы использовать searchField, а не поле description в качестве отфильтрованного поля:

const SearchIceCreams = gql`
  query($searchQuery: String) {
    listIceCreams(filter: {
      searchField: {
        contains: $searchQuery
      }
    }) {
      items {
        name
        description
      }
    }
  }
`

Затем мы преобразуем любой запрос в нижний регистр, прежде чем отправлять его в наш API. В функции onSearch мы добавим для этого новую строку кода перед вызовом props.data.fetchMore:

// above code omitted
props: props => ({
  onSearch: searchQuery => {
    searchQuery = searchQuery.toLowerCase() // added
    return props.data.fetchMore({
    // below code omitted

Теперь мы должны иметь возможность выполнять поиск по полям имени и описания и получать результаты!

Окончательная кодовая база находится здесь.

Меня зовут Надер Дабит. Я адвокат разработчиков в AWS Mobile, работаю с такими проектами, как AWS AppSync и AWS Amplify, и основатель React Native Training.

Если вам понравилась эта статья, пожалуйста, хлопните n раз и поделитесь ею! Спасибо за ваше время.