Русский
Русский
English
Статистика
Реклама

Блог компании naumen

Методологический скачок от таблиц-портянок к понятному каталогу услуг в ITSM-системе

14.10.2020 14:07:29 | Автор: admin


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

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

Эволюция подхода к созданию сервисного каталога


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

Таблицы в Word. Еще несколько лет назад по каскадной модели разработки (Waterfall) скрупулезно собирали информацию в текстовые файлы. В этих документах фиксировали всё от наименования услуг, основных ответственных до видов деятельности по определенной услуге и SLA.

image

Пример табличного описания услуги Электронная почта в формате текстового документа

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



К концу аудита услуг заполненные таблицы могли исчисляться десятками


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

В то же время в любой компании процессы не статичны. Появляются новые сервисы, развиваются существующие. Услуги обрастают огромным слоем объектов: запросами, справочниками, связями, параметрами, КЕ.

Сколько бы ни длилась разработка документа, конца и края не видать. Документ сложно согласовать, да и его пригодность сомнительна. Это как с ремонтом, который нельзя закончить, а можно только приостановить. Так и в наших прошлых проектах работа с многостраничными свитками обычно заканчивалась фразой: Довольно! Пора перебираться в систему.

Таблицы в Excel. Взамен многостраничных документов в нашу практику ворвались другие таблицы. Описательную часть для клиента мы по-прежнему оставляли текстом. А к нему прикладывали таблицу со списком услуг и их параметрами. Сложить нужную информацию в один документ опять не получалось. По факту каталог услуг вырастал в сеть взаимосвязанных таблиц.



Сопровождать в Excel множество вкладок и столбцов вручную неудобно. На эту работу аналитик внедрения тратил сотни часов


Такой способ составления списка услуг нами рассматривался как почва для первичного импорта в систему автоматизации. Далее развитие и оптимизацию планировали вести уже в ИТ-системе. Но и этот подход оказался не без минусов.

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

Другое дело динамический каталог, который изначально проектируется со своей структурой и ценностью в инструменте автоматизации. Подобный каталог начинает функционировать только с появления в нем первой услуги. Нельзя на этапе внедрения разработать каталог услуг под ключ. За этот период удается запустить процесс по его наполнению и актуализации. А значит оптимальный подход собрать минимум информации и сразу идти в ИТ-систему.

В чем ценность каталога услуг



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

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



Интерфейс Портала самообслуживания с удобной группировкой услуг. Реализован на базе платформы Naumen SMP

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

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

Владельцам и Заказчикам интересны параметры качества и финансов.

Какие шаги помогут создать каталог услуг в ИТ-системе


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

Шаг 1. Определение цели


Главный вопрос в любой деятельности зачем?. Для себя цели сервисного каталога сформулировали так:

  1. Организовать продуктивное взаимодействие с получателями услуг.
  2. Использовать единую платформу для построения ITSM-процессов и применения сервисного подхода во всех подразделениях компании.
  3. Заложить основу для управления и развития всех бизнес-процессов.

Шаг 2. Проведение обследования


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

В наше время с нуля это как вернуться в каменный век или попробовать заново изобрести велосипед. Не стоит отказываться от наработок. Берем все, что есть, и упаковываем в ИТ-систему.

Если каталог услуг в компании не формализован, анализируем такие артефакты:

  • журнал обращений;
  • (если используются) классификаторы и справочники для заявок;
  • список информационных систем и оборудования;
  • оргструктуру;
  • сервисные контракты с внешними поставщиками.

Далее алгоритм следующий:

  1. Выделяем популярные вопросы к сервисным службам.
  2. Формируем набор услуг на понятном пользователю языке.
  3. Группируем и приземляем обращения на управляемые ресурсы.
  4. Предлагаем наименование услуги, которое увидит пользователь в ИТ-системе при подаче обращения.



Пример корреляции: Обращение пользователя-Оборудование-Услуга в ИТ-системе


Изначально в каталоге стоит предусмотреть услугу Прочее/Заявка в свободной форме. Негласно ее называют Канал подачи мусора. При периодическом анализе подобных обращений можно определить:

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

Шаг 3. Распределение ответственности


Когда стартовый набор услуг готов, необходимо установить и зафиксировать исполнителей.

Важно определить уровни ответственности:

  • Исполнитель обращений.
  • Менеджер услуги.
  • Менеджер каталога.

Исполнителя найти проще. Для этого в опоре на статистику журнала обращений сопоставляем должностные инструкции с функциональными обязанностями и компетенциями.

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

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

  • Руководитель проекта внедрения ПО. Видит картину в целом и может принимать управляющие централизованные решения.
  • Руководитель сервисной организации (ИТ-департамента). Наиболее заинтересован в развитии взаимоотношений заказчиков и исполнителей, владеет знаниями стратегии сервисной организации и знает, как они коррелируют со стратегией основного бизнеса.
  • Группа компетенций. Ответственность закрепляется не за конкретным специалистом, а формируется на основе знаний участников проекта внедрения: РП, главных консультантов, сотрудников, которые способны передавать знания другим участникам.

Шаг 4. Подготовка каталога


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

Здесь сразу делимся нашими наработкам: какие параметры на практике оказались наиболее востребованными и зачем они нужны.



Готовый перечень параметров услуги. Скачать полную версию

Результатом формирования каталога услуг в ИТ-системе становится качественный классификатор, при этом еще обновляемый и управляемый.

Шаг 5. Развитие каталога


Важный этап развития каталога услуг настройка связи с ресурсно-сервисной моделью (РСМ). Ее проектирование и поддержка может поглощать бесконечные трудоресурсы. Но пользы от нее гораздо больше.

Ценность этой связи покажем на простом примере. В компании не работает интернет. Инцидент зарегистрирован. Именно РСМ поможет быстрее узнать, в чем источник проблемы (по связям с поддерживающим оборудованием) и какие зависящие от этого узла сервисы будут отваливаться дальше.

Финальный аккорд расписать взаимозависимости между услугами, добавить перечень КЕ и завести в ИТ-систему процесс управления конфигурациями. И главное жить по процессу, чтобы все это нежно собранное не расплескать!



Пример части каталога услуг, который одновременно служит классификатором КЕ


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

Как машинное обучение спасает деревья в Екатеринбурге

03.08.2020 18:13:27 | Автор: admin


Привет, Хабр! Мы сотрудники екатеринбургского офиса NAUMEN. Делимся интересным проектом интерактивной картой деревьев на основе нейросетевых алгоритмов. В ее создании также участвуют студенты УрФУ и волонтеры нашего города.


Многие современные мегаполисы сталкиваются с проблемой недостатка зеленых насаждений, иЕкатеринбург не исключение. Точечная застройка центральных районов и, как следствие, незаконные вырубки, сокращение парковых массивов не помогают улучшить ситуацию. Думаем, эта проблема знакома жителям многих российских городов. Одно из решений создать регулярно обновляемую интерактивную карту деревьев, доступную всем. С ее помощью можно будет узнавать, сколько деревьев уже растет в городе, сколько их должно быть и в каких районах необходимы дополнительные посадки.


Проводить опись зеленых насаждений вручную долго и неэффективно. На помощь приходят современные технологии: аэрофотосъемка, изображения Street View, съемка LiDAR-систем идронов. Среди многообразия источников данных для получения информации об объектах городской среды наиболее доступными и относительно дешевыми являются панорамные фотографии со Street View. С их помощью можно обучить нейросеть распознавать на снимках деревья, определять их геолокацию и наносить их на карту города. Как проходил этот процесс икаких результатов удалось добиться читайте в статье.



Зачем Екатеринбургу интерактивная карта деревьев


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



По оценке качества городской среды Екатеринбург набрал меньше всего баллов за озеленение пространства


Практика подсчета деревьев реализована во многих городах мира. Открытые реестры зеленых насаждений есть в Вене, Амстердаме, Праге. Наиболее удачный пример карта деревьев центрального района Нью-Йорка. Свыше 2200 сотрудников муниципалитета и волонтеров заодин год нанесли на нее порядка 700 000 деревьев и указали их вид. Теперь, подтвердив навыки ухода за насаждениями, любой человек может взять опеку над деревом. Каждый житель Нью-Йорка может пожаловаться на состояние дерева или проблемы с ним.


О создании подобного сервиса в Екатеринбурге давно задумывались в сообществе волонтеров Парки и скверы, куда входят Илья Котельников и Борис Оводов, сотрудники компании NAUMEN. Ребята решили помочь создать интерактивную карту деревьев Екатеринбурга, ориентируясь на данные которой жители смогут влиять на озеленение города и предотвращать незаконные вырубки.



По задумке, все деревья Екатеринбурга можно будет увидеть на интерактивной карте


В январе 2020 года сообщество волонтеров выполнило первоначальную настройку системы наресурсе ekbtrees.ru. За две недели они нанесли на карту 150 деревьев, растущих в саду Вайнера. Собирать данные вручную оказалось очень долго, и оперативное обновление информации вбудущем представлялось невозможным. Поэтому пришли к решению использовать технологии машинного обучения.


Илья Котельников:


Используя интеллектуальные алгоритмы, мыхотим создать систему, которая посчитает, сколько деревьев вЕкатеринбурге. Базовая задача защитить то, что есть, апотом раз в35 лет проводить инвентаризацию.
В идеале создать такой механизм, с помощью которого горожане смогут получать полезные сервисы: заказывать высадку зеленых насаждений или уход заними, жаловаться наповаленные исломанные деревья, сообщать о незаконных действиях.


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


Создание интерактивной карты с использованием нейросети


Чтобы ускорить процесс сбора информации о деревьях города, Илья обратился к Татьяне Зобниной, специализирующейся на разработке систем машинного обучения. Он предложил ей попробовать реализовать интерактивную карту с помощью нейросетевых алгоритмов. Изучив накопленный опыт в области распознавания деревьев и определения геопозиции объектов поданным Street View и других источников, Татьяна убедилась, что задача решаема. В январе 2020 года она разместила проект на сайте Проектного практикума УрФУ, чтобы привлечь кразработке студентов.


Татьяна Зобнина:


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



Созданием интерактивной карты деревьев при помощи алгоритмов машинного обучения заинтересовались 4 студента Радиотехнического факультета: Егор Дёмин, Егор Кипелов, Александр Черных и Георгий Шишкин. Все они в прошлом семестре уже проходили у Татьяны курс Распознавание эмоций по звуку и имели достаточно высокий уровень технологических знаний и навыков, чтобы реализовать этот проект.


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


  • детектирование деревьев по аэрофотоснимкам;
  • использование фотографий со Street View.

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


По итогам исследования оказалось, что несмотря на все преимущества аэрофотосъемки качественных данных по Екатеринбургу в открытом доступе нет. От этого решения на данном этапе развития проекта пришлось отказаться. Тогда обе группы вместе приступили к реализации варианта со Street View.


Ребятам предстояло:


  1. Написать код для выгрузки данных со Street View.
  2. Разметить выгруженные фотографии выделить на них деревья.
  3. Обучить нейронную сеть распознавать деревья на фотографиях.
  4. Написать код для преобразования фотографии со Street View, на которых выделено дерево, в координаты, оценить их точность.
  5. Загрузить данные на интерактивную карту деревьев Екатеринбурга.
  6. Проверить точность расположения деревьев на карте с помощью волонтеров.

Первая гипотеза: обучить нейросеть с помощью карты деревьев NY и снимков со Street View


Google и Yandex предоставляют доступ к огромному количеству городских панорам. При этом существует обширная база деревьев Нью-Йорка с геопозицией каждого растения, но безснимков. Появилась гипотеза, что можно обогатить базу фотографиями и с ее помощью обучить нейросеть по картинке определять координаты, а также другие параметры деревьев: высоту, диаметр, видовую принадлежность. После можно будет использовать обученную нейросеть, чтобы распознать деревья на панорамах Екатеринбурга и указывать иххарактеристики, включая геолокацию.


Однако у этого метода есть свои минусы. Дело в том, что карта деревьев Нью-Йорка была создана в 2015 году, а съемка Street View содержит более актуальную информацию. Соответственно, данные на карте и на фотографиях могут не совпадать, из-за чего в системе могут возникать ошибки. Тем не менее, это было лучшим решением для обучения нейросети наданном этапе, и ребята приступили к его реализации.


Для работы над проектом компания NAUMEN предоставила вычислительные мощности наодном из своих серверов. Студенты написали код для выгрузки данных со Street View, оптимизировали его на своих ноутбуках и перенесли на сервер.


Дальше нужно было определиться с подходящей для проекта нейросетью. Ребята остановили свой выбор на Open Source решении сверточной нейросети YOLOv3. Основная задача, которую студенты ставили перед ней, классифицировать размеченные данные и распознавать деревья на снимках со Street View.


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


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



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


Помимо этого, нашими наработками заинтересовались в МГТУ им. Н.Э. Баумана. Оказалось, что Михаил Ухов, участвовавший в сборе данных для карты деревьев Нью-Йорка в качестве волонтера, реализует подобный проект в Москве. Чтобы помочь коллегам, Татьяна поделилась дообученной YOLOv3.


Тем временем студенты написали код, который при помощи нейросети распознает и выделяет на панорамах со Street View деревья и позволяет получить их координаты. Однако во время тестов выяснилось, что такой подход не дает нужной точности в определении геолокации дерева. Непростой по форме объект и искажение, которое присутствует на снимках Street View, препятствует получению правильной широты и долготы расположения зеленых насаждений.



Искажение на снимках со Street View мешает нейросети точно определять геопозицию деревьев


Вторая гипотеза: определение геопозиции по снимкам Street View Екатеринбурга


Чтобы понять, как точнее определять координаты деревьев по фотографиям и в каком направлении двигаться вместе со студентами дальше, Татьяна снова приступила кисследованию. Оказалось, что есть готовое решение, описанное нашим соотечественником, который преподает в университете Дублина, Владимиром Крыловым. В статье он описал алгоритм, позволяющий точно распознавать геопозицию столбов электропередач и светофоров по Street View, и разместил ссылку на GitHub с кодом алгоритма.


Гипотетически описанный Владимиром алгоритм мог подойти для распознавания деревьев. Налетней стажировке в NAUMEN студенты занялись доработкой и компоновкой кода и настроек. Также подключили к работе дообученную нейросеть YOLOv3.


Получился пайплайн из трёх нейросетей и алгоритма, работающий следующим образом:


  1. Система собирает снимки со Street View.
  2. Первая нейросеть сегментирует деревья, выделяя их силуэт на фотографии.
  3. YOLOv3 уточняет сегмент, выделяя дерево в квадратную рамку.
  4. Третья нейросеть определяет расстояние от машины, с которой шла съемка Street View, донужного объекта в нашем случае до дерева.
  5. Далее алгоритм геотегирования определяет координаты дерева.

Этот метод позволяет сразу определять координаты деревьев на снимках Екатеринбурга, пропустив этап обучения нейросети с помощью карты деревьев Нью-Йорка.


В результате первого теста таким способом удалось распознать по снимкам Street View 68 деревьев по улице Сурикова и определить их координаты.



Так на карте обозначаются метки распознанных деревьев на одной из улиц Екатеринбурга


Следующим шагом планируется подключить волонтеров и оценить точность полученных данных. По итогам проверки студенты УрФУ и сотрудники NAUMEN продолжат работу надпроектом в следующем семестре.


Отметим, что обучение нейросетевых алгоритмов продолжается, поэтому любой желающий может подключиться к проекту и помочь. Для этого необходимо скачать телеграм-бота и отсортировать размеченные фотографии. Более весомый вклад можно внести, размечая подобранные фотографии самостоятельно в программе YoloLabel. Если хотите поучаствовать, пишите в комментариях.

Подробнее..

Fetch библиотека для доступа к данным

24.07.2020 12:09:40 | Автор: admin

Fetch это библиотека Scala для организации доступа к данным из файловых систем, БД, веб-сервисов и любых других источников, данные из которых можно получить по уникальному идентификатору. Библиотека написана в функциональном стиле и основана на Cats и Cats Effect. Предназначена для композиции и оптимизации выполнения запросов к разным источникам данных. Она позволяет:


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

Для этого в библиотеке предоставляются средства, которые позволяют писать чистый бизнес-код без низкоуровневых конструкций для осуществления перечисленных оптимизаций.
В примерах используется последняя на момент написания версия Fetch 1.3.0.


Источник данных в Fetch


Для реализации доступа к какому-либо источнику через Fetch требуется имплементировать:


  • описание источника данных (трейт Data[I, A]);
  • методы получения данных из источника (трейт DataSource[F[_], I, A]).

DataSource[F[_], I, A] (где I тип идентификатора, по которому требуется что-то получить: например, путь к файлу или ID в базе данных; A тип результата, а F тип эффекта) содержит эффективные методы для извлечения из него данных:


/** * A `DataSource` is the recipe for fetching a certain identity `I`, which yields * results of type `A` performing an effect of type `F[_]`. */trait DataSource[F[_], I, A] {  def data: Data[I, A]  implicit def CF: Concurrent[F]  /** Fetch one identity, returning a None if it wasn't found.   */  def fetch(id: I): F[Option[A]]  /** Fetch many identities, returning a mapping from identities to results. If an   * identity wasn't found, it won't appear in the keys.   */  def batch(ids: NonEmptyList[I]): F[Map[I, A]] =    FetchExecution.parallel(      ids.map(id => fetch(id).tupleLeft(id))    ).map(_.collect { case (id, Some(x)) => id -> x }.toMap)  def maxBatchSize: Option[Int] = None  def batchExecution: BatchExecution = InParallel}

data: Data[I, A] содержит ссылку на экземпляр Data[I,A], который является описанием источника. CF это доказательство того, что выбранный эффект экземпляр Concurrent. Метод fetch это метод получения самих данных по ID. batch это тоже метод получения данных, но он предназначен для одновременного получения нескольких ID из источника. Изначально он описан в терминах fetch и не требует имплементации просто запускает всю пачку ID параллельно. Иногда его полезно переопределить: многие источники данных позволяют получить больше одного элемента за раз, например, реляционные базы данных.


Простейший пример оборачивания списка в термины Fetch:


class ListData(val list: List[String]) extends Data[Int, String] {  override def name: String = "My List of Data"}class ListDataSource(list: ListData)(implicit cs: ContextShift[IO])    extends DataSource[IO, Int, String]    with LazyLogging {  override def data: ListData = list  /*implicit дает Stack overflow, видимо он начинает крутить сам себя */  override def CF: Concurrent[IO] = Concurrent[IO]  override def fetch(id: Int): IO[Option[String]] =    CF.delay {      logger.info(s"Processing element from index $id")      data.list.lift(id)    }}

Обычно эти структуры совмещают: экземпляр DataSource вкладывают в класс-наследник Data. Это позволяет немного сжать код и хранить всё в одном месте:


class ListSource(list: List[String])(implicit cf: ContextShift[IO]) extends Data[Int, String] with LazyLogging {  override def name: String        = "My List of Data"  private def instance: ListSource = this  def source = new DataSource[IO, Int, String] {    override def data: Data[Int, String] = instance    override def CF: Concurrent[IO] = Concurrent[IO]    override def fetch(id: Int): IO[Option[String]] =      CF.delay {        logger.info(s"Processing element from index $id")        list.lift(id)      }  }}

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


Сами по себе методы DataSource нельзя использовать напрямую. Нужно передавать их в специальные объекты Fetch. Эти объекты что-то вроде "чертежей" для получения данных. Они содержат в себе идентификатор данных и источник. Затем их нужно передавать в методы объекта Fetch вроде Fetch.run (помимо run есть ещё несколько специальных методов, возвращающих дополнительные данные). Эти объекты создают из источника и ID эффект с ответом. В целом, это может быть любой эффект F, для которого есть экземпляр Concurrent[F]. Эта манипуляция выглядит следующим образом:


val list                                = List("a", "b", "c")val data: ListSource                    = new ListSource(list)val source: DataSource[IO, Int, String] = data.sourceval fetchDataPlan: Fetch[IO, String] = Fetch(1, source)val fetchData: IO[String]            = Fetch.run(fetchDataPlan)val dataCalculated: String           = fetchData.unsafeRunSync // b

Оборачивание в специальный объект, хранящий ID и источник, позволяет выполнять библиотеке описанные выше оптимизации.


Полный пример:


object Example extends App {  implicit val ec: ExecutionContext = global  implicit val cs: ContextShift[IO] = IO.contextShift(ec)  // для Fetch.run и ListDataSource  implicit val timer: Timer[IO]     = IO.timer(ec) // для Fetch.run  val list = List("a", "b", "c")  val data   = new ListSource(list)  val source = data.source  Fetch.run(Fetch(0, source)).unsafeRunSync    // INFO ListDataSource - Processing element from index 0  Fetch.run(Fetch(1, source)).unsafeRunSync    // INFO ListDataSource - Processing element from index 1  Fetch.run(Fetch(2, source)).unsafeRunSync    // INFO ListDataSource - Processing element from index 2  Fetch.run(Fetch(3, source)).unsafeRunSync    // INFO ListDataSource - Processing element from index 3  // Exception in thread "main" fetch.package$MissingIdentity}

Последний вызов выбросил исключение, хотя data.list.lift(id) в методе fetch успешно защищает от несуществующих индексов. Исключение бросается в ситуациях, когда fetch возвращает None. Это связано с тем, что источник не может возвращать Option или содержать Option в качестве одного из типов: DataSource[F[_], I, A]. Но запросить Option можно явно, создав объект Fetch не через метод apply, а через optional:


Fetch.run(Fetch.optional(3, source)).unsafeRunSync  // None

Разницу можно понять, просто взглянув на типы:


val fApply: Fetch[IO, String]            = Fetch(3, source)val fOptional: Fetch[IO, Option[String]] = Fetch.optional(3, source)

Иногда внутрь классов-наследников Data помещают специальный метод, избавляющий от необходимости вручную составлять объект Fetch. Он может быть, как обычным, так и optional:


def fetchElem(id: Int) = Fetch.optional(id, source)

Теперь возможно переписать используемые методы на заранее подготовленный fetchElem в классе ListSource:


Fetch.run(data.fetchElem(0)).unsafeRunSync // INFO app.ListDataSource - Processing element from index 0Fetch.run(data.fetchElem(1)).unsafeRunSync // INFO app.ListDataSource - Processing element from index 1Fetch.run(data.fetchElem(2)).unsafeRunSync // INFO app.ListDataSource - Processing element from index 2println(Fetch.run(data.fetchElem(2)).unsafeRunSync) // Some(c)println(Fetch.run(data.fetchElem(3)).unsafeRunSync) // None

Кэширование


Fetch не кэширует результаты из коробки:


def fetch(id: Int): Option[String] = {  val run   = Fetch.run(data.fetchElem(id))  run.unsafeRunSync}fetch(1)  // INFO app.ListDataSource - Processing element from index 1fetch(1)  // INFO app.ListDataSource - Processing element from index 1fetch(1)  // INFO app.ListDataSource - Processing element from index 1

Для кэширования в Fetch существует специальный трейт DataCache[F[_]]. Сама библиотека предоставляет одну готовую имплементацию неизменяемый кэш InMemoryCache[F[_]: Monad](state: Map[(Data[Any, Any], DataSourceId), DataSourceResult]). Им можно воспользоваться через методы его объекта-компаньона from создать кэш из готовой коллекции; и empty создать пустой кэш:


def from[F[_]: Monad, I, A](results: ((Data[I, A], I), A)*): InMemoryCache[F] def empty[F[_]: Monad]: InMemoryCache[F]

В обоих случаях в методах происходят преобразования, приводящие к тому, что кэш хранится в структуре данных типа Map[(Data[Any, Any], DataSourceId), DataSourceResult]. Дополнительные типы в этом сопоставлении:


final class DataSourceId(val id: Any)         extends AnyValfinal class DataSourceResult(val result: Any) extends AnyVal

Получается, что ключ этого сопоставления кортеж (Data[Any, Any], DataSourceId). Он содержит источник данных любого типа и ID любого типа. Значение сопоставления DataSourceResult. Он содержит результат любого типа. Исходя из этого понятно, что в один кэш можно складывать результаты работы Fetch с самыми различными источниками данных. Ещё из этого следует, что конкретные типы при записи в кэш стираются все данные имеют тип Any при хранении. Но они не остаются такими при извлечении. В случае с InMemoryCache извлечение из кэша происходит следующим образом:


def lookup[I, A](i: I, data: Data[I, A]): F[Option[A]] =  Applicative[F].pure(    state      .get((data.asInstanceOf[Data[Any, Any]], new DataSourceId(i)))      .map(_.result.asInstanceOf[A])  )

Тут важно, что Data часть ключа. Именно из переданного экземпляра Data берётся тип A, к которому методом asInstanceOf[A] приводится хранимый в кэше тип Any при запросе. Вставка в этот кэш работает на основе обычного метода Map updated, который возвращает новую коллекцию при изменении.


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


Пример использования созданного кэша методом from:


val cacheF: DataCache[IO] = InMemoryCache.from((data, 1) -> "b", (data, 2) -> "c")Fetch.run(data.fetchElem(1), cacheF).unsafeRunSync  // взялось из кэшаFetch.run(data.fetchElem(1), cacheF).unsafeRunSync  // прочитано из кэшаFetch.run(data.fetchElem(1), cacheF).unsafeRunSyncFetch.run(data.fetchElem(1), cacheF).unsafeRunSyncFetch.run(data.fetchElem(0), cacheF).unsafeRunSyncFetch.run(data.fetchElem(0), cacheF).unsafeRunSync// INFO app.ListDataSource - Processing element from index 0// INFO app.ListDataSource - Processing element from index 0

Видно, что кэширование работает, хотя кэш и не пополняется новыми элементами. Уже ясно, что это происходит из-за устройства кэша неизменяемая коллекция Map возвращает новую коллекцию при любом изменении. Это означает, что кэшем нужно управлять вручную. Для работы с этим поведением существует метод Fetch.runCache, который возвращает кортеж типа (обновленный кэш, результат):


var cache: DataCache[IO] = InMemoryCache.emptydef cachedRun(id: Int): Option[String] = {  val (c, r) = Fetch.runCache(Fetch.optional(id, source), cache).unsafeRunSync  cache = c  // Пример ручного управления кэшем  r}cachedRun(1)cachedRun(1)cachedRun(2)cachedRun(2)cachedRun(4)cachedRun(4)// INFO app.ListDataSource - Processing element from index 1// INFO app.ListDataSource - Processing element from index 2// INFO app.ListDataSource - Processing element from index 4// INFO app.ListDataSource - Processing element from index 4

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


Пример: использование Caffeine для кэширования


Для подключения собственной библиотеки кэширования нужно лишь имплементировать трейт DataCache. Полученный класс позволит использовать библиотеку в любых вызовах Fetch. Следующим образом можно реализовать DataCache для известной Java-библиотеки Caffeine, а точнее для её обёртки на Scala Scaffeine:


class ScaffeineCache extends DataCache[IO] with LazyLogging {  private val cache =    Scaffeine()      .recordStats()      .expireAfterWrite(1.hour)      .maximumSize(500)      .build[(Data[Any, Any], Any), Any]()  override def lookup[I, A](i: I, data: Data[I, A]): IO[Option[A]] = IO {    cache      .getIfPresent(data.asInstanceOf[Data[Any, Any]] -> i)      .map { any =>        val correct = any.asInstanceOf[A]        logger.info(s"From cache: $i")        correct      }  }  override def insert[I, A](i: I, v: A, data: Data[I, A]): IO[DataCache[IO]] = {    cache.put(data.asInstanceOf[Data[Any, Any]] -> i, v) // Unit    IO(this)  }}

Здесь используются всё те же приёмы, что и в InMemoryCache. Так как Scaffeine работает с типизированными кэшами кэш создаётся с типами Any: build[(Data[Any, Any], Any), Any](). Затем запись и получение данных из него производится с использованием asInstanceOf:


val list  = List("a", "b", "c")val listSource  = new ListSource(list)val source = listSource.sourceval randomSource = new RandomSource()val cache = new ScaffeineCache()/** Без кэширования */Fetch.run(Fetch(1, source)).unsafeRunSync // Processing element from index 1Fetch.run(Fetch(1, source)).unsafeRunSync // Processing element from index 1println()/** С кэшированием */Fetch.run(Fetch(1, source), cache).unsafeRunSync // Processing element from index 1Fetch.run(Fetch(1, source), cache).unsafeRunSync // From cache: 1Fetch.run(Fetch("a", source), cache).unsafeRunSync // type mismatch/** Один кэш подходит разным источникам */Fetch.run(randomSource.fetchInt(2), cache).unsafeRunSync  // Getting next random by max 2Fetch.run(randomSource.fetchInt(2), cache).unsafeRunSync  // From cache: 2

Можно заметить несколько вещей:


  • при попытке использовать кэш с неправильным типом ID (чтобы попытаться нарушить работу asInstanceOf) произойдёт type mismatch по причине создания объекта Fetch с ID и Source, не подходящими друг другу по типам;
  • один и тот же кэш действительно можно использовать для разных источников;
  • благодаря внутреннему устройству Caffeine, нам не нужно вручную управлять изменениями кэша мы просто передаём одну и ту же ссылку в каждый вызов. Несмотря на это, трейт DataCache всё равно требует возвращать ссылку на кэш в методе insert.

Комбинаторы


В Fetch возможно использовать различные комбинаторы из Scala и Cats. Смысл этих комбинаторов преобразовать тип List[Fetch[_,_]] в Fetch[_, List[_]], который затем передаётся в Fetch.run. Это необходимо для осуществления всех видов оптимизаций Fetch: объединения запросов, комбинирования, дедупликации, запусках в параллели. Оптимизации Fetch распространяются только на то, что передано внутри объекта Fetch в текущем Fetch.run Единственное исключение кэширование, которое работает между запросами.


Важно правильно использовать комбинаторы. Например:


val t1: IO[(String, String)] = for {  a <- Fetch.run(Fetch(1, source))  b <- Fetch.run(Fetch(1, source))} yield (a, b)

В этом примере for использован неверно: мы не получили Fetch[_, List[_]] или подобный тип, все объекты Fetch были переданы в run отдельно, поэтому и выполнены будут отдельно. Правильно будет использовать for следующим образом:


val f1: Fetch[IO, (String, String)] = for {  a <- Fetch(1, source)  b <- Fetch(1, source)} yield (a,b)val t2: IO[(String, String)] = Fetch.run(f1)

Теперь мы получили Fetch от кортежа. Это значит, что при передаче в Fetch.run произойдут оптимизации:


  • дедупликация;
  • объединение если в кортеже есть запросы к одинаковому источнику;
  • комбинирование если в кортеже есть запросы к разным источникам.

В зависимости от настроек DataSource и имплементации метода batch объединение может происходить в параллели.


Того же эффекта позволяют добиться методы из Cats. Если описывать просто, метод sequence создаёт из коллекции List[F[_]] коллекцию F[List[_]]. Метод traverse делает почти то же самое: из коллекции List[] делает `F[List[]]`. Наконец, tupled делает из кортежа эффектов один эффект со значением кортежа:


val f3: List[Fetch[IO, String]] = List(  Fetch(1, source),  Fetch(2, source),  Fetch(2, source))val f31: Fetch[IO, List[String]] = f3.sequenceval t3: IO[List[String]]         = Fetch.run(f31)val f4: List[Int] = List(  1,  2,  2)val f41: Fetch[IO, List[String]] = f4.traverse(Fetch(_, source))val t4: IO[List[String]]         = Fetch.run(f41)val f5: (Fetch[IO, String], Fetch[IO, String]) = (Fetch(1, source), Fetch(2, source))val f51: Fetch[IO, (String, String)]           = f5.tupledval t5: IO[(String, String)]                   = Fetch.run(f51)val f6: (Int, Int)                = (1, 2)val f61: Fetch[IO, (Int, String)] = f6.traverse(Fetch(_, source))

Не стоит забывать и о flatMap:


val f0: Fetch[IO, String]  = Fetch(1, source).flatMap(_ => Fetch(1, source))val t0: IO[String]         = Fetch.run(f0)

Как видно из типов, совершенно неважно, каким методом был получен Fetch от нескольких запросов. Возможно использовать любые подходящие комбинаторы Cats. Комбинирование объектов Fetch главный приём для работы с библиотекой, ведь без него никакие оптимизации работать не будут. Кроме того, именно от выбранного метода комбинирования зависит, будут ли запросы выполнены последовательно или параллельно.


Объединение запросов (Batching)


Fetch может объединять запросы к одному источнику в один запрос. Например:


import fetch.fetchM  // инстансы Fetch для синтаксиса Catsval tuple: Fetch[IO, (Option[String], Option[String])] = (data.fetchElem(0), data.fetchElem(1)).tupledFetch.run(tuple).unsafeRunSync()  // (Some(a),Some(b))

По умолчанию метод batch в DataSource описан в терминах fetch и запрашивает идентификаторы у источника в параллели. Его можно переопределить. Например, для реляционной базы данных там может быть SQL-запрос для получения сразу множества ключей.


Поместим логгер в batch в источнике в ListSource:


override def batch(ids: NonEmptyList[Int]): IO[Map[Int, String]] = {  logger.info(s"IDs fetching in batch: $ids")  super.batch(ids)}

Выполняем пакетный запрос через уже известный traverse:


import fetch.fetchM def findMany: Fetch[IO, List[Option[String]]] =  List(0, 1, 2, 3, 4, 5).traverse(data.fetchElem)Fetch.run(findMany).unsafeRunSync// INFO app.ListSource - IDs fetching in batch: NonEmptyList(0, 5, 1, 2, 3, 4)

Можно ограничить размер, переопределив метод maxBatchSize:


override def maxBatchSize: Option[Int] = 2.some  // defaults to None// INFO app.ListSource - IDs fetching in batch: NonEmptyList(0, 5)// INFO app.ListSource - IDs fetching in batch: NonEmptyList(1, 2)// INFO app.ListSource - IDs fetching in batch: NonEmptyList(3, 4)

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


override def batchExecution: BatchExecution = Sequentially // defaults to `InParallel`

Комбинирование данных из разных источников


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


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


class RandomSource(implicit cf: ContextShift[IO]) extends Data[Int, Int] with LazyLogging {  override def name: String          = "Random numbers generator"  private def instance: RandomSource = this  def source: DataSource[IO, Int, Int] = new DataSource[IO, Int, Int] {    override def data: Data[Int, Int] = instance    override def CF: Concurrent[IO] = Concurrent[IO]    override def fetch(max: Int): IO[Option[Int]] =      CF.delay {        logger.info(s"Getting next random by max $max")        scala.util.Random.nextInt(max).some      }  }}

Мы можем запросить эти данные в одном Fetch вместе с запросом из listSource:


val listSource = new ListSource(List("a", "b", "c"))val randomSource = new RandomSource()def fetchMulti: Fetch[IO, (Int, String)] =  for {    rnd <- Fetch(3, randomSource.source)  // Fetch[IO, Int]    char <- Fetch(rnd, listSource.source)  // Fetch[IO, String]  } yield (rnd, char)println(Fetch.run(fetchMulti).unsafeRunSync)  // например, (0,a)

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


Параллельное и последовательное выполнение


В целом, возможность параллельного запуска зависит от использованного комбинатора. Например, for и flatMap предполагают только последовательный запуск, так как мы можем передавать переменные в следующие этапы вычисления. sequence и traverse позволяют выполнять запросы к одному источнику параллельно и объединённо. С другой стороны, если у двух разных источников одинаковые типы, то sequence и traverse можно использовать для параллельного чтения из них. Но лучше в этой ситуации подходит метод tupled, который позволяет делать параллельные запросы к разным источникам с разными типами. Оценить получившуюся последовательность запуска помогут методы дебага, описанные ниже.


Дедупликация запросов


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


Дедупликация в объединенных запросах:


val list = List("a", "b", "c", "d", "e", "f", "g", "h", "i")val data = new ListSource(list, 2.some)val tupleD: Fetch[IO, (Option[String], Option[String])] = (data.fetchElem(0), data.fetchElem(0)).tupledFetch.run(tupleD).unsafeRunSync()//INFO app.sources.ListSource - Processing element from index 0//(Some(a),Some(a))

Дедупликация в комбинированных:


def fetchMultiD: Fetch[IO, (Int, String, Int, String)] =  for {    rnd1 <- Fetch(3, randomSource.source)  // Fetch[IO, Int]    char1 <- Fetch(rnd1, listSource.source)  // Fetch[IO, String]    rnd2 <- Fetch(3, randomSource.source)  // Fetch[IO, Int]    char2 <- Fetch(rnd2, listSource.source)  // Fetch[IO, String]  } yield (rnd1, char1, rnd2, char2)println(Fetch.run(fetchMultiD).unsafeRunSync)//18:43:11.875 [scala-execution-context-global-14] INFO app.sources.RandomSource - Getting next random by max 3//18:43:11.876 [scala-execution-context-global-13] INFO app.sources.ListSource - Processing element from index 1//(1,b,1,b)

Обработка исключений


В Fetch предоставлены инструменты для работы с исключениями:


  • общий трейт FetchException;
  • специальное исключение MissingIdentity для несуществующих в источнике ID;
  • общее исключение UnhandledException для любых остальных исключений.

Мы уже запускали Fetch с результатом Option, но есть более безопасная альтернатива, возвращающая Either:


// val i: String = Fetch.run(Fetch(5, data.source)).unsafeRunSync // Exception in thread "main" fetch.package$MissingIdentityval i: Either[Throwable, String] = Fetch.run(Fetch(5, data.source)).attempt.unsafeRunSync // Left(fetch.package$MissingIdentity)

Дебаг Fetch


Fetch предоставляет средства дебага в виде метода Fetch.runLog, возвращающего объект FetchLog с историей работы. В отдельной библиотеке fetch-debug предоставлен метод describe, который красиво обрабатывает Throwable и Log.


Пример вывода описания throwable:


// libraryDependencies += "com.47deg" %% "fetch-debug" % "1.3.0"import fetch.debug.describeval t: Either[Throwable, (Log, String)] = Fetch.runLog(Fetch(5, data.source)).attempt.unsafeRunSyncprintln(t.fold(describe, identity))// [ERROR] Identity with id `5` for data source `My List of Data` not found, fetch interrupted after 1 rounds// Fetch execution 0.00 seconds////    [Round 1] 0.00 seconds//      [Fetch one] From `My List of Data` with id 5 0.00 seconds

Напишем сложный запрос со всеми изученными функциями Fetch (>> в Cats это альяс для flatMap(_ => ...)):


object DebugExample extends App with ContextEntities {  val list = List("a", "b", "c", "d", "e", "f", "g", "h")  val listData                                = new ListSource(list)  val listSource: DataSource[IO, Int, String] = listData.source  val randomSource                            = new RandomSource().source  val cacheF: DataCache[IO] = InMemoryCache.from((listData, 1) -> "b")  // нет среди раундов вообще  val cached = Fetch(1, listSource)  // только #1, больше не повторяется  val notCached = Fetch(2, listSource)  // #2  val random = Fetch(10, randomSource)  // #3  val batched: Fetch[IO, (String, String)] = (Fetch(3, listSource), Fetch(4, listSource)).tupled  // #4  val combined = (Fetch(5, listSource), Fetch(150, randomSource)).tupled  /** End of fetches */  val complicatedFetch: Fetch[IO, (String, Int)] = cached >> notCached >> random >> notCached >> batched >> combined  val result: IO[(Log, (String, Int))]           = Fetch.runLog(complicatedFetch, cacheF)  val tuple: (Log, (String, Int))                = result.unsafeRunSync()  println(tuple._2) // (f,17)  println(describe(tuple._1))  println(tuple._1)  //Fetch execution 0.11 seconds  //  //  [Round 1] 0.06 seconds  //    [Fetch one] From `My List of Data` with id 2 0.06 seconds  //  [Round 2] 0.00 seconds  //    [Fetch one] From `Random numbers generator` with id 10 0.00 seconds  //  [Round 3]  0.01 seconds  //    [Batch] From `My List of Data` with ids List(3, 4)  0.01 seconds  //  [Round 4]  0.00 seconds  //    [Fetch one] From `Random numbers generator` with id 150  0.00 seconds  //    [Fetch one] From `My List of Data` with id 5  0.00 seconds  // raw:  // FetchLog(Queue(Round(List(Request(FetchOne(2,app.sources.ListSource@ea6147e),10767139,10767203))), Round(List(Request(FetchOne(10,app.sources.RandomSource@58b31054),10767211,10767213))), Round(List(Request(Batch(NonEmptyList(3, 4),app.sources.ListSource@ea6147e),10767234,10767242))), Round(List(Request(FetchOne(150,app.sources.RandomSource@58b31054),10767252,10767252), Request(FetchOne(5,app.sources.ListSource@ea6147e),10767252,10767252)))))}

Можно сделать несколько наблюдений:


  • cached вообще не появляется в логах, ведь он не считается никогда;
  • notCached посчитался лишь раз. Cработала оптимизация и повторный запрос дедуплицировался;
  • batch явно прописывается как запрос пакетом;
  • комбинированный запрос к нескольким источникам выглядит в логах как один раунд с несколькими запросами.

Действия в пределах одного раунда происходят параллельно. Сами по себе раунды разделены методом >>, который предполагает только последовательный запуск. А вот в раундах 3 и 4 использован метод tupled, который и позволяет запускать запросы параллельно. В первом случае это запрос к одному источнику, который был объединён. Во втором случае этот запрос был к разным источникам, поэтому он был запущен параллельно.


Пример: запрос данных из реальных источников


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


  • полнотекстовый поиск для получения ID документов по какому-то поисковому запросу;
  • сервис информации о документе;
  • сервис получения имени автора документа из его ID;
  • сервис похожих документов.

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


/*Model*/type DocumentId = Stringtype PersonId = Stringcase class FtsResponse(ids: List[DocumentId])case class SimilarityItem(id: DocumentId, similarity: Double)case class DocumentInfo(id: DocumentId, info: String, authors: List[PersonId])case class Person(id: PersonId, fullTitle: String)/*Response*/case class DocumentSearchResponse(    items: List[DocumentSearchItem])case class DocumentItem(id: DocumentId, info: Option[String], authors: List[Person])case class DocumentSimilarItem(    item: DocumentItem,    similarity: Double)case class DocumentSearchItem(    item: DocumentItem,    similar: List[DocumentSimilarItem])class DocumentSearchExample(    fts: Fts[IO],    documentInfoRepo: DocumentInfoRepo[IO],    vectorSearch: VectorSearch[IO],    personRepo: PersonRepo[IO])(    implicit cs: ContextShift[IO]) {  val infoSource    = new DocumentInfoSource(documentInfoRepo, 16.some)  val personSource  = new PersonSource(personRepo, 16.some)  val similarSource = new SimilarDocumentSource(vectorSearch, 16.some)  def documentItemFetch(id: DocumentId): Fetch[IO, DocumentItem] =    for {      infoOpt <- infoSource.fetchElem(id)      p       <- infoOpt.traverse(i => i.authors.traverse(personSource.fetchElem).map(_.flatten))    } yield DocumentItem(id, infoOpt.map(_.info), p.getOrElse(List.empty[Person]))  def fetchSimilarItems(id: DocumentId): Fetch[IO, List[DocumentSimilarItem]] =    similarSource      .fetchElem(id)      .map(_.getOrElse(List.empty[SimilarityItem]))      .flatMap {        _.traverse { si =>          documentItemFetch(si.id).map { di =>            DocumentSimilarItem(di, si.similarity)          }        }      }  def searchDocumentFetch(query: String): Fetch[IO, DocumentSearchResponse] =    for {      docs <- Fetch.liftF(fts.search(query))      items <- docs.ids.traverse { id =>                (documentItemFetch(id), fetchSimilarItems(id)).tupled.map(r => DocumentSearchItem(r._1, r._2))              }    } yield DocumentSearchResponse(items)}

На вход DocumentSearchExample получает три репозитория. Это сами сервисы с кодом, который обращается непосредственно к хранилищам данных. Например, там может быть код обращения к БД на Doobie. Далее они передаются в обёртки Fetch DocumentInfoSource, PersonSource и SimilarDocumentSource, которые позволяют оптимизировать запросы к ним.


Метод searchDocumentFetch служит для произведения полнотекстового поиска по запросу query. Сам по себе поиск выполняется отдельно от остальных запросов, поэтому для него используется сервис напрямую, без обёртки в Fetch. Сигнатура search:


def search(query: String): F[FtsResponse]

Поэтому метод liftF позволяет получить тип Fetch[F, FtsResponse], который можно использовать в for наряду с остальными Fetch.


Далее для списка ID запрашиваются данные документа и ID похожих документов. Кортеж элементов Fetch соединён через tupled, что позволяет сделать эти вызовы параллельно, ведь они не зависят друг от друга. Наконец, возвращённые в этом вызове значения мапятся в тип DocumentSearchItem.


Метод fetchSimilarItems получает ID похожих документов из similarSource, а затем их информацию из метода documentItemFetch. В этот же метод обращаемся и при получении данных оригинальных документов. Он получает информацию о документах DocumentInfo по ID, а затем и имена авторов, указанных в информации.


Использование свойств монад позволяет композировать множество вызовов в один объект вместо вложенных вроде Fetch[IO, Fetch[...]] благодаря методам map и flatMap:


similarSource  .fetchElem(id)  .map(_.getOrElse(List.empty[SimilarityItem]))  // Fetch  .flatMap {     _.traverse { si =>      documentItemFetch(si.id).map { di =>  // Fetch        DocumentSimilarItem(di, si.similarity)      }    }  }

Для примера реализуем все репозитории на простых коллекциях:


private val docInfo = Map(  "1" -> DocumentInfo("1", "Document 1", List(1)),  "2" -> DocumentInfo("2", "Document 2", List(1,2)),  "3" -> DocumentInfo("3", "Document 3", List(3,1)),  "4" -> DocumentInfo("4", "Document 4", List(2,1)),  "5" -> DocumentInfo("5", "Document 5", List(2)),  "6" -> DocumentInfo("6", "Document 6", List(1,3)))private val similars = Map(  "1" -> List(SimilarityItem("2", 0.7), SimilarityItem("3", 0.6)),  "2" -> List(SimilarityItem("1", 0.7)),  "3" -> List(SimilarityItem("1", 0.6)),  "4" -> List(),  "5" -> List(SimilarityItem("6", 0.5)),  "6" -> List(SimilarityItem("5", 0.5)))private val persons = Map(  1 -> Person(1, "Rick Deckard"),  2 -> Person(2, "Roy Batty"),  3 -> Person(3, "Joe"))

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


INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 2. It is: Some(List(1))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 4. It is: NoneINFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 3. It is: Some(List(1))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 1. It is: Some(List(2, 3))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 6. It is: Some(List(5))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 5. It is: Some(List(6))INFO app.searchfetchproto.source.DocumentInfoSource - Document IDs fetching in batch: NonEmptyList(4, 5, 2, 3, 6, 1)INFO app.searchfetchproto.source.PersonSource - Person IDs fetching in batch: NonEmptyList(1, 2, 3)

На самом деле, поиск похожих документов тоже осуществляется параллельно, ведь для него не описано отдельного метода batch:


[Round 1]  0.12 seconds    [Batch] From `Similar Document Source` with ids List(4, 5, 2, 3, 6, 1) 0.06 seconds    [Batch] From `Document Info Source` with ids List(4, 5, 2, 3, 6, 1) 0.12 seconds  [Round 2]  0.00 seconds    [Batch] From `Persons source` with ids List(1, 2, 3)  0.00 seconds

В коде метода searchDocumentFetch есть момент:


(documentItemFetch(id), fetchSimilarItems(id)).tupled

Эти запросы должны выполняться параллельно благодаря tupled. И, хотя fetchSimilarItems запускает свой собственный documentItemFetch, из логов видно, что обращение в сервис информации о документах происходит единожды и всегда после обращения в сервис похожих:


[Batch] From `Similar Document Source` with ids List(4, 5, 2, 3, 6, 1) 0.06 seconds[Batch] From `Document Info Source` with ids List(4, 5, 2, 3, 6, 1) 0.12 seconds

Это происходит при незначительной задержке сервиса похожих документов, когда результат первого запроса приходит быстрее, чем начинается следующий запрос. Такой результат используется в следующем запросе и позволяет ещё больше оптимизировать запросы. Если же искусственно повысить время выполнения (например, добавив Thread.sleep(100) в сервис), то можно наблюдать такую картину:


INFO app.searchfetchproto.source.DocumentInfoSource - Document IDs fetching in batch: NonEmptyList(4, 5, 2, 3, 6, 1)INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 4. It is: NoneINFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 5. It is: Some(List(6))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 3. It is: Some(List(1))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 2. It is: Some(List(1))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 6. It is: Some(List(5))INFO app.searchfetchproto.source.SimilarDocumentSource - Fetching similar documents for ID: 1. It is: Some(List(2, 3))INFO app.searchfetchproto.source.DocumentInfoSource - Document IDs fetching in batch: NonEmptyList(5, 2, 3, 6, 1)  [Round 1]  0.13 seconds    [Batch] From `Similar Document Source` with ids List(4, 5, 2, 3, 6, 1)  0.13 seconds    [Batch] From `Document Info Source` with ids List(4, 5, 2, 3, 6, 1)  0.08 seconds  [Round 2]  0.00 seconds    [Batch] From `Document Info Source` with ids List(5, 2, 3, 6, 1)  0.00 seconds    [Batch] From `Persons source` with ids List(1, 2, 3)  0.00 seconds

Четвёртый документ не является похожим ни на один из документов, поэтому не запрашивается во время выполнения fetchSimilarItems. Поэтому явно видно, что второй запрос относится к методу поиска похожих.


Выводы


Использование Fetch позволяет писать чистый композируемый код для эффективного доступа к источникам данных в функциональном стиле. Дополнительные конструкции вроде кэширования, дедупликации, объединения запросов и комбинирования писать не требуются, такой функционал предоставляется самой библиотекой. Благодаря использованию стека Cats, Fetch может быть интегрирована в большое количество существующих программ на Scala и отлично уживается с библиотеками вроде Doobie и fs2.


Аналоги


  • ZQuery судя по описанию, делает всё то же самое, но на стеке ZIO вместо Cats;
  • Clump видимо, предшественник Fetch, заброшенный в 2015;
  • Haxl то же самое на Haskell.

ZQuery и Fetch практические реализации статьи There is no Fork: an Abstraction for Efficient, Concurrent, and Concise Data Access (https://simonmar.github.io/bib/papers/haxl-icfp14.pdf), абстрактно описывающей (с привязкой к Хаскеллю) возможность программировать доступ к источникам данных на аппликативных функторах.


Ссылки


Подробнее..

Онбординг как мы адаптируем сотрудников на удалёнке

11.11.2020 12:15:25 | Автор: admin

Онбординг (или адаптация) сотрудников на новом месте не всегда проходит гладко, а удалёнка добавляет ещё больше сложностей. Год назад мы скептически относились к такому формату. Теперь спокойно нанимаем сотрудников в штат по всей стране и делимся опытом с вами.

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

Преонбординг

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

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

У сотрудника возникает ощущение, что его ждали. И это правда так, поэтому мы создали все условия. Компания от этого только выиграет: чем комфортнее человеку на новом месте, тем раньше он начнет себя проявлять.

Что мы готовим к выходу сотрудника?

  • узнаем, есть ли дома необходимая техника. Если нет организуем доставку из офиса;

  • необходимые доступы к внутренним сервисам;

  • план работ на время испытательного срока;

  • назначаем наставника и менеджера по адаптации перед ними стоит ответственная миссия: влюбить новичка в компанию.

Первый день в компании

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

В NAUMEN освоиться помогает менеджер по адаптации, как правило, это HR, который сопровождал сотрудника во время найма. После оформления необходимых документов на почту новичку приходит welcome-памятка, а через пару минут по видеосвязи звонит менеджер, чтобы провести адаптационную беседу.

О чем говорим на первой встрече?

  • Рассказываем о том, чем занимается компания: история, продукты, значимые награды и достижения.

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

  • Объясняем, как и к кому можно обращаться. Например, в NAUMEN стараются избегать формальностей и сотрудники общаются друг с другом на ты.

  • Говорим о корпоративной культуре, ценностях и возможностях развития.

После беседы с менеджером по адаптации подключается наставник и знакомит новичка с планом работ на время испытательного срока.

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

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

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

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

Первые три месяца в компании

Первые три месяца работы у новичков достаточно насыщенные. Им предстоит адаптироваться сразу в четырёх направлениях:

  • привыкнуть к коллегам и их стилю коммуникации в команде;

  • принять и освоить корпоративные правила;

  • перестать испытывать стресс из-за смены работы;

  • адаптироваться к рабочим процессам и самостоятельно выполнять рабочие задачи.

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

Несмотря на то что вся моя адаптация проходила на удаленке, мне было достаточно комфортно влиться не только в свою команду, но и познакомиться с другими коллегами. Отзывчивость коллег это одно из преимуществ адаптации в NAUMEN, на мой взгляд. Здесь всегда посоветуют, куда обратиться с проблемой. Ты достаточно быстро осваиваешься в большой структуре подразделений, понимаешь, к кому и по каким вопросам нужно обращаться. Быстрое решение организационных моментов это еще одно преимущество адаптации в NAUMEN Аня, аналитик NAUMEN.

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

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

Также ответы на многие вопросы новенькие могут получить, не прибегая к помощи коллег, (не все же сотрудники должны быть экстравертами, верно?). На корпоративном портале под ответы на вопросы отведены два раздела. Первый для совсем новичков с базовой информацией о работе в компании, второй будет полезен абсолютно всем: здесь можно узнать где пообедать, как уйти на больничный или как рассчитывается заработная плата.

Performance Review

Заключительный этап онбординга Performance Review или PR. Это встреча, на которой новый сотрудник, руководитель и наставник обмениваются обратной связью и впечатлениями о том, как прошли эти три месяца. Цель PR по окончании испытательного срока подвести взаимный итог успешности и результативности достижения задач из плана работ.

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

Не нужно бояться задавать свои вопросы. Для полноценной адаптации на удаленке есть множество способов общения: видеосвязь, общий чат, личные сообщения наставнику. Чтобы адаптация прошла быстрее, удобно подключаться к видеоконференциям, которые периодически устраиваются в компании. На них можно узнать много интересного о том, какие сейчас задачи решают коллеги, познакомиться с ними поближе, быстрее понять структуру подразделений, кто и чем занимается Аня, аналитик NAUMEN.

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru