Учебник

Недавно у нас был второй хакатон на RigUp, и моя команда заняла первое место в категории клиентов ! Наше решение включало настраиваемые скребки, очереди, ETL данных и запросы к полученным данным. Затем мы написали приложение React, чтобы обеспечить удобную визуализацию для отображения этих данных. 📈 Поскольку у нас было всего 48 часов, наш код не был готов к работе.

В этом руководстве исследуется мой подход к улучшению проекта в свободное время. Я решил написать сервер GraphQL для доступа к нашим геоданным ElasticSearch. Я выбрал язык Scala, использующий библиотеки Akka Http и Sangria. Следует отметить, что RigUp - это не дом Scala, просто Scala - это большая часть моего опыта.

Для нашего хакатона у нас было несколько индексов ElasticSearch для разных типов документов, но у каждого типа документа была широта и долгота. Это потребовало от нас выполнения нескольких запросов API к ElasticSearch, что означает дублирование большой части логики поиска. Поскольку мы просматривали данные на карте, мы использовали Elastic Geo Bounding Box Queries.

// Pseudocode
fetch('/users', { geo_bounding_box, ...some_filter_here });
fetch('/coffee-shops', { geo_bounding_box, ...other_filter });

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

Ссылка на $ bbox будет указывать на объект “bbox”, который мы определяем на панели «Переменные запроса» в graphiql. Мы фильтруем пользователей, которые находятся за пределами этого поля.

Ниже приводится сильное вдохновение из документации Sangria, а также из учебника HowToGraphQL Scala. Мы создадим запросы GraphQL для фильтрации пользователей и кафе по имени и геолокации.

Настройка проекта

Весь финальный код доступен на GitHub.

ElasticSearch

Самая простая установка - с помощью следующего файла docker-compose или установка Elastic локально и запуск на порт по умолчанию (9200).

Запуск docker-compose up -d должен помочь вам начать работу. Убедитесь, что вы можете получить ответ от ElasticSearch в своем браузере по адресу localhost: 9200.

Чтобы загрузить наших тестовых пользователей и кафе, перейдите в папку сценариев и запустите сценарий загрузки:

$ cd src/main/resources/scripts/
$ ./load-test-data.sh

Переход по адресу http: // localhost: 9200 / test-users / _search? Pretty должен дать вам список пользователей.

Зависимости

Ресурсы

Давайте добавим graphiql.html в наш каталог ресурсов (в /src/main/resources), чтобы у нас была игровая площадка для тестирования наших запросов GraphQL. Ниже мы увидим маршрут, который будет обслуживать этот файл из каталога ресурсов.

Главный Сервер

Есть всего два пути; один принимает POST в / graphql, а другой обслуживает статические ресурсы. Прямо сейчас выполнение запроса POST к конечной точке graphql просто вернет строку. Создаем Main.scala под /src/main/scala

Модели

Модели можно разделить на несколько категорий; переменные, ответы и общие. Переменные - это классы, которые будут сопоставлены аргументам GraphQL. Ответы будут ответами сервера GraphQL и будут напрямую связаны со схемой GraphQL. Эти входы и выходы могут использовать общие модели, это станет ясно вскоре.

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

    ├── Main.scala
    └── models
        ├── common
        │   └── Location.scala
        ├── responses
        │   ├── CoffeeShop.scala
        │   ├── SearchResponse.scala
        │   └── User.scala
        └── variables
            ├── BBox.scala
            └── Filter.scala

Общий

Location - простой класс case со свойствами latitude и longitude.

Переменные

Давайте создадим классы Bounding Box и Filter. BBox принимает два объекта Location, topLeft и bottomRight как свойства, а Filter принимает необязательные BBox и необязательные String объекты как свойства. topLeft и bottomRight соотносятся с углами нашего экрана, на которых отображается карта.

Ответы

Давайте создадим общий SearchResponse класс:

Класс дела User будет иметь свойства name, id, и location. Свойство location будет объектом типа Location.

Наш UsersResponse класс case будет содержать свойство total и список пользователей.

Модели CoffeeShop и CoffeeShopResponse очень похожи на модели User и User Response:

Основной код

Давайте продолжим и создадим / добавим следующие файлы в /src/main/scala

├── Elastic.scala
├── GraphQLSchema.scala
├── GraphQLServer.scala
├── Main.scala
└── models

Схема GraphQL

Для более подробного ознакомления с определениями схем Sangria и GraphQL см. Документы Sangria.

Здесь мы привязываем наши модели к схеме GraphQL:

ElasticSearch

В Учебнике HowToGraphQL автор использует Slick и базу данных H2 в памяти для сохранения и запроса данных. Мы будем использовать elastic4s. Мы создаем трейт для хранения нашей эластичной конфигурации и методов (упрощает тестирование):

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

В Sangria есть концепция контекста, которая сочетается с запросами GraphQL. Это очень важно для нашего варианта использования, поскольку он будет содержать экземпляр нашего класса Elastic и переменную BBox.

Мы можем определить наш контекст как простой класс case в GraphQLSchema.scala:

case class MyContext(elastic: Elastic, bbox: Option[BBox] = None)

Сервер GraphQL

Теперь мы создадим GraphQL Server, который будет иметь Akka Http Route и экземпляр нашего Elastic класса. Приведенный ниже метод конечной точки принимает JsValue, анализирует его и возвращает Route.

Вы увидите, что мы звоним на executeGraphQLQuery. Давайте построим это дальше:

Здесь мы передаем наш экземпляр Elastic, а также ранее определенный GraphQLSchema.SchemaDefinition. Не забудьте обновить Main.scala для маршрутизации запросов к нашему GraphQLServer:

Тестируем нашу установку

Для запуска нашего сервера мы используем sbt. Из корневого каталога:

$ sbt ~reStart

Перейдите к localhost: 8080 и введите наш запрос и переменную bbox:

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

{
  "data": {
    "geoSearch": {
      "users": {
        "hits": [
          {
            "id": 3,
            "name": "Ricky", 
            "location": ...
          },
          {
            "id": 4,
            "name": "Carter", 
            "location": ...
          },
          {
            "id": 5,
            "name": "Mitch", 
            "location": ...
          }
        ]
      }
    }
  }
}

Давайте настроим нашу ограничивающую рамку, чтобы покрыть большую площадь:

Мы видим четвертого пользователя, который находится в Сан-Диего, Калифорния:

{
  "id": 1,
  "name": "Duane", 
  "location": {
    "lat": "32.715736",
    "lon": "-117.161087"
  }
}

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

И мы видим нашего пятого и последнего пользователя в Мехико 🇲🇽

{
  "name": "Matt",
  "id": 2,
  "location": {
    "lat": "19.42847",
    "lon": "-99.12766"
  }
}

Расширение нашей установки

Последний кусок головоломки - добавление дополнительного поля для поиска и фильтрации наших пользователей. Мы собираемся добавить фильтр имени, чтобы мы могли делать такие запросы:

Чтобы выполнить фильтрацию по имени, обновим метод buildQuery в Elastic.scala:

Теперь, если мы запустим новый запрос сверху, мы увидим, что вернулись только Мэтт и Митч! Очень легко добавить новые функции.

{
  "data": {
    "geoSearch": {
      "users": {
        "hits": [
          {
            "id": 2,
            "name": "Matt",
            "location": {
              "lat": "19.42847",
              "lon": "-99.12766"
            }
          },
          {
            "id": 5,
            "name": "Mitch",
            "location": {
              "lat": "30.366666",
              "lon": "-97.833330"
            }
          }
        ],
        "total": 2
      }
    }
  }
}

Наконец, допустим, мы хотим найти пользователей в нашем географическом поле с именами, начинающимися с буквы «M», а также кафе в районе с названием «Starbucks».

{
  "data": {
    "geoSearch": {
      "users": {
        "hits": [
          {
            "id": 2,
            "name": "Matt",
            "location": {
              "lat": "19.42847",
              "lon": "-99.12766"
            }
          },
          {
            "id": 5,
            "name": "Mitch",
            "location": {
              "lat": "30.366666",
              "lon": "-97.833330"
            }
          }
        ],
        "total": 2
      },
      "coffeeShops": {
        "hits": [
          {
            "name": "Starbucks"
          },
          {
            "name": "Starbucks"
          }
        ],
        "total": 2
      }
    }
  }
}

Мы можем быстро подключить простое приложение React к Mapbox и Apollo для отображения некоторых наших данных (исходного кода):

npx create-react-app gql-elastic-app
cd gql-elastic-app/
npm install apollo-boost @apollo/react-hooks graphql
npm install react-mapbox-gl mapbox-gl --save

Давайте запустим наш UserFeatures компонент, который будет сопоставлять наших пользователей с функциями Mapbox:

Теперь мы добавляем строку запроса GraphQL:

Давайте настроим Mapbox в нашем компоненте приложения:

Теперь об Аполлоне:

Затем нам нужно добавить некоторое состояние для ограничивающей рамки карты:

Быстрая служебная функция для преобразования объекта Bounds Mapbox в наш собственный BBox объект:

Наконец, мы добавляем два обработчика, которые принимают события карты и обновляют наше локальное состояние:

Запустив приложение, вы переместитесь в Остин, штат Техас, точно так же, как на GIF-изображении выше, и отобразит 3 пользователя. Если мы переместим карту в Сан-Диего, мы увидим нашего 4-го пользователя:

Вы также должны увидеть нашего пятого пользователя в Мехико! Если бы вы следовали инструкциям, вы бы заметили ошибку CORS. Это простое исправление, и оно было рассмотрено в Main.scala в репо.

Надеюсь, вам понравился гео-поиск с помощью GraphQL и ElasticSearch! Весь код с открытым исходным кодом. 😎 Приветствуются PR, комментарии и т. Д.!

Бэкэнд-код Scala - https://github.com/duanebester/gql-elastic-scala

Код веб-интерфейса React - https://github.com/duanebester/gql-elastic-app