Учебник
Недавно у нас был второй хакатон на 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