[TL; DR; Начните использовать продукт Slash GraphQL от Dgraph и подключитесь к приложению Spring Boot, которое будет действовать как простая служба рекомендаций RESTful.]

Еще в начале 2000-х я работал над проектом по внедрению решения для электронной коммерции от Art Technology Group (ATG), ныне принадлежащей Oracle. Продукт ATG Dynamo оказался впечатляющим решением, так как он включал в себя уровень сохраняемости и модуль сценариев. В то время такие компании, как Target и Best Buy, использовали решение Dynamo, используя модуль сценария для предоставления рекомендаций клиенту.

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

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

О примере рекомендаций

Я хотел упростить задачу и создать несколько базовых доменных объектов для механизма рекомендаций. В этом примере решение будет давать рекомендации для музыкальных исполнителей, а базовый объект Artist довольно прост:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Artist {
  private String name;
}

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

Как и следовало ожидать, клиенты будут оценивать художников по шкале от 1 до 5, где значение 5 представляет собой наилучшую возможную оценку. Конечно, возможно (и ожидается), что клиенты не будут оценивать каждого художника. Клиент будет представлен (снова) очень простым объектом Customer:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Customer {
  private String username;
}

Понятие клиентской оценки художника будет отражено в следующем объекте рейтинга:

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Rating {
  private String id;
  private double score;
  private Customer by;
  private Artist about;
}

В моих обычных усилиях по программированию на Java я бы, вероятно, использовал для своих объектов частного клиента Customer и частного художника Artist, но я хотел следовать шаблону, используемому в графовых базах данных, где вместо этого я использую такие переменные, как `by` и` about`. станет более ясным по мере продолжения статьи.

Dgraph Slash GraphQL

Учитывая популярность графовых баз данных, я почувствовал, что в моем исследовании создания механизма рекомендаций должна использоваться графическая база данных. В конце концов, GraphQL стал популярным языком для разговоров с сервисами о графах. Хотя у меня есть только некоторые знания о графовых базах данных, мой анализ, похоже, пришел к выводу, что графовая база данных является правильным выбором для этого проекта и часто является источником рекомендаций для реальных сервисов. Графические базы данных - отличное решение, когда отношения (границы) между вашими данными (узлами) так же важны, как и сами данные, и механизм рекомендаций является прекрасным примером.

Однако, поскольку я только начинаю работать с графическими базами данных, я определенно не хотел беспокоиться о запуске контейнера или локальном запуске базы данных GraphQL. Вместо этого я хотел найти поставщика SaaS. Я решил использовать полностью управляемую внутреннюю службу Dgraph под названием Slash GraphQL. Это размещенное собственное решение GraphQL. Сервис Slash GraphQL был выпущен 10 сентября 2020 года, и его можно включить, перейдя по следующей ссылке. Платформа предлагает бесплатную пробную версию, которая будет работать для этой статьи (затем будет установлена ​​фиксированная плата в размере 9,99 долларов США в месяц за данные объемом до 5 ГБ).

Https://slash.dgraph.io/

После запуска этого URL-адреса новую учетную запись можно создать с помощью обычных сервисов авторизации:

В моем примере я создал бэкэнд под названием «spring-boot-demo», результатом которого стала следующая панель:

Процесс начала работы был быстрым и бесплатным, что позволило легко настроить службу Slash GraphQL.

Настройка Slash GraphQL

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

type Artist {
  name: String! @id @search(by: [hash, regexp])
  ratings: [Rating] @hasInverse(field: about)
}
type Customer {
  username: String! @id @search(by: [hash, regexp])
  ratings: [Rating] @hasInverse(field: by)
}
type Rating {
  id: ID!
  about: Artist!
  by: Customer!
  score: Int @search
}

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

Имея схему, я смог быстро заполнить некоторую базовую информацию об исполнителе:

mutation {
  addArtist(input: [
    {name: “Eric Johnson”},
    {name: “Genesis”},
    {name: “Led Zeppelin”},
    {name: “Rush”},
    {name: “Triumph”},
    {name: “Van Halen”},
    {name: “Yes”}]) {
    artist {
      name
    }
  }
}

В то же время я добавил несколько фиктивных записей клиентов:

mutation {
  addCustomer(input: [
    {username: “Doug”},
    {username: “Jeff”},
    {username: “John”},
    {username: “Russell”},
    {username: “Stacy”}]) {
    customer {
      username
    }
  }
}

В результате пять заказчиков предоставят оценки семи артистам, используя следующую таблицу:

Пример рейтингового процесса показан ниже:

mutation {
  addRating(input: [{
    by: {username: “Jeff”},
    about: { name: “Triumph”},
    score: 4}])
  {
    rating {
      score
      by { username }
      about { name }
    }
  }
}

С настроенными и запущенными данными Slash GraphQL я теперь могу переключать передачи и работать со службой Spring Boot.

Алгоритм оценки Slope One

В 2005 году в исследовательской статье Даниэля Лемира и Анны Маклачиан было представлено семейство алгоритмов совместной фильтрации Slope One. Эта простая форма совместной фильтрации на основе элементов выглядела идеально подходящей для службы рекомендаций, поскольку она учитывает рейтинги других клиентов для оценки элементов, не оцененных для данного клиента.

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

  • получить рейтинги, доступные для всех художников (по всем клиентам)
  • создать карту ‹Клиент, Карта‹ Художник, Двойник ›› на основе данных, которая представляет собой карту клиента, содержащую всех художников и их рейтинги
  • рейтинг от 1 до 5 будет преобразован в простой диапазон от 0,2 (худший рейтинг из 1) до 1,0 (лучший рейтинг из 5).

После создания карты клиентов ядро ​​обработки рейтингов Slope One будет выполняться путем вызова класса SlopeOne:

  • заполнить карту ‹Художник, Карта‹ Художник, Двойник ›› используется для отслеживания различий в рейтингах от одного покупателя к другому
  • заполнить карту ‹Художник, Карта‹ Художник, Целое число ›› используется для отслеживания частоты похожих оценок
  • использовать существующие карты для создания карты ‹Customer, HashMap‹ Artist, Double ››, которые содержат прогнозируемые оценки для элементов, не оцененных для данного клиента

В этом примере выбирается случайный клиент и анализируется соответствующий объект из карты Map ‹Customer, HashMap‹ Artist, Double ›› projectedData и возвращаются следующие результаты:

{
  “matchedCustomer”: {
    “username”: “Russell”
  },
  “recommendationsMap”: {
    “Artist(name=Eric Johnson)”: 0.7765842245950264,
    “Artist(name=Yes)”: 0.7661904474477843,
    “Artist(name=Triumph)”: 0.7518039724158979,
    “Artist(name=Van Halen)”: 0.7635436007978691
  },
  “ratingsMap”: {
    “Artist(name=Genesis)”: 0.4,
    “Artist(name=Led Zeppelin)”: 1.0,
    “Artist(name=Rush)”: 0.6
  },
  “resultsMap”: {
    “Artist(name=Eric Johnson)”: 0.7765842245950264,
    “Artist(name=Genesis)”: 0.4,
    “Artist(name=Yes)”: 0.7661904474477843,
    “Artist(name=Led Zeppelin)”: 1.0,
    “Artist(name=Rush)”: 0.6,
    “Artist(name=Triumph)”: 0.7518039724158979,
    “Artist(name=Van Halen)”: 0.7635436007978691
  }
}

В приведенном выше примере случайным образом был выбран пользователь «Рассел». Глядя на исходную таблицу (выше), Рассел предоставил оценки только для Genesis, Led Zeppelin и Rush. Единственным артистом, которым он искренне восхищался, был Led Zeppelin. Эта информация включается в объект ratingMap, а также в объект resultsMap.

Объект resultsMap включает прогнозируемые рейтинги для остальных четырех художников: Эрика Джонсона, Yes, Triumph и Van Halen. Чтобы упростить задачу, в полезную нагрузку включена карта рекомендаций, которая включает только художников, не оцененных Расселом.

Основываясь на других обзорах, Служба рекомендаций отдала бы небольшое предпочтение Эрику Джонсону по сравнению с остальными четырьмя пунктами - с оценкой 0,78, что почти равно четырем в пятибалльной рейтинговой системе.

Служба рекомендаций

Чтобы использовать службу рекомендаций, сервер Spring Boot просто должен быть запущен и настроен для подключения к облачному экземпляру Slash GraphQL. Конечная точка GraphQL на панели инструментов Slash GraphQL может быть указана в application.yml как slash-graph-ql.hostname или путем передачи значения через переменную среды $ {SLASH_GRAPH_QL_HOSTNAME}.

Базовый механизм рекомендаций можно вызвать с помощью следующего RESTful URI:

GET — {spring-boot-service-host-name}/recommend

Это действие настраивается контроллером рекомендаций, как показано ниже:

@GetMapping(value = “/recommend”)
public ResponseEntity<Recommendation> recommend() {
  try {
    return new ResponseEntity<>(recommendationService.recommend(), HttpStatus.OK);
  } catch (Exception e) {
    return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
  }
}

Что вызывает Рекомендацию:

@Slf4j
@RequiredArgsConstructor
@Service
public class RecommendationService {
  private final ArtistService artistService;
  private final CustomerService customerService;
  private final SlashGraphQlProperties slashGraphQlProperties;
  private static final String RATING_QUERY = “query RatingQuery { queryRating { id, score, by { username }, about { name } } }”;
  public Recommendation recommend() throws Exception {
    ResponseEntity<String> responseEntity = RestTemplateUtils.query(slashGraphQlProperties.getHostname(), RATING_QUERY);
    try {
      ObjectMapper objectMapper = new ObjectMapper();
      SlashGraphQlResultRating slashGraphQlResult = objectMapper.readValue(responseEntity.getBody(), SlashGraphQlResultRating.class);
      log.debug(“slashGraphQlResult={}”, slashGraphQlResult);
      return makeRecommendation(slashGraphQlResult.getData());
    } catch (JsonProcessingException e) {
      throw new Exception(“An error was encountered processing responseEntity=” + responseEntity.getBody(), e);
    }
  }
…
}

Обратите внимание: кое-что, что может быть упущено при беглом взгляде на этот код, - это мощь и простота возможности вытащить подграф для выполнения рекомендации. В приведенном выше примере строка slashGraphQlResult.getData () предоставляет подграф для метода makeRecommendation ().

RATING_QUERY - это ожидаемый формат Slash GraphQL для получения объектов рейтинга. Метод RestTemplateUtils.query () является частью статического служебного класса, чтобы все было СУХИМ (не повторяйтесь):

public final class RestTemplateUtils {
  private RestTemplateUtils() { }
  private static final String MEDIA_TYPE_GRAPH_QL = “application/graphql”;
  private static final String GRAPH_QL_URI = “/graphql”;
  public static ResponseEntity<String> query(String hostname, String query) {
    RestTemplate restTemplate = new RestTemplate();
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.valueOf(MEDIA_TYPE_GRAPH_QL));
    HttpEntity<String> httpEntity = new HttpEntity<>(query,   headers);
    return restTemplate.exchange(hostname + GRAPH_QL_URI, HttpMethod.POST, httpEntity, String.class);
  }
}

После получения объекта slashGraphQlResult вызывается закрытый метод makeRecommendation (), который возвращает следующий объект Рекомендации. (Это было показано выше в формате JSON):

@AllArgsConstructor
@NoArgsConstructor
@Data
public class Recommendation {
  private Customer matchedCustomer;
  private HashMap<Artist, Double> recommendationsMap;
  private HashMap<Artist, Double> ratingsMap;
  private HashMap<Artist, Double> resultsMap;
}

Вывод

В этой статье был создан экземпляр Dgraph Slash GraphQL с новой схемой и загружены образцы данных. Затем эти данные использовались службой загрузки Spring, которая служила базовым механизмом рекомендаций. Тем, кто интересуется полным исходным кодом, просим ознакомиться с репозиторием GitLab:

С точки зрения затрат я весьма впечатлен структурой, которую предоставляет Slash GraphQL. Экран нового аккаунта показал, что у меня есть 10 000 кредитов в месяц, которые я могу использовать бесплатно. За все время, пока я использовал Slash GraphQL для прототипирования и создания всего для этой статьи, я использовал только 292 кредита. Для тех, кому требуется более 10 000 кредитов, 45 долларов США в месяц позволяют получить 100 000 кредитов, а 99 долларов США в месяц позволяют получить 250 000 кредитов.

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

В моей следующей статье я познакомлю с этим процессом клиента Angular (или React), который будет напрямую взаимодействовать с GraphQL и службой рекомендаций, работающей в Spring Boot.

(опубликовать с любезного разрешения автора Джон Вестер)