Flow, подобно модулям Enum и Stream, позволяет разработчикам производить вычисления в коллекциях, однако с его помощью и с помощью GenStage вычисления могут выполняться параллельно.
Канонический пример, размещённый в сервисе GitHub, показывает, как с помощью Flow можно параллельно осуществить подсчёт количества слов в документе:
Мне хотелось опробовать Flow в действии ещё с тех пор, когда Жозе Валим только представил концепции работы этого модуля на конференции "Elixir Conf 2016". У меня сразу же возникла мысль разработать инструмент для построения системы рекомендаций на основе отзывов большого количества людей (краудсорсинг). Так как это предполагает наличие задач с интенсивным вводом-выводом (HTTP-запросы) и операций с высокой вычислительной нагрузкой на CPU (например, анализ тональности текста), то параллельное исполнение и Flow – то, что нужно.
Краудсорсинг я использовал для того, чтобы найти и оценить подходящие учебные материалы: книги, электронные книги, видеоролики и обучающие курсы.
Алгоритм ранжирования основан на совокупности показателей популярности и результатов анализа тональности. Популярные материалы с высоким рейтингом имеют высокую оценку. Оценка – совокупность положительных, отрицательных и нейтральных отзывов пользователей известного форума об Elixir.
Создание инструмента рекомендации товаров
Разработанное мной зонтичное приложение (umbrella application) на Elixir состоит из трёх приложений:
indexer, осуществляющего выборку внешнего контента с помощью HTTP-запросов и упорядочивающего товары по рейтингу.
recommendations, содержащего область Ecto schema и запросы: ресурсы, отзывы, оценки, авторы.
web – фронтенд-части приложения в Phoenix, реализующей вывод рекомендованных товаров.
Flow используется в приложении indexer, где производятся все вычисления. Я набросал схему этапов конвейера, после чего разработал отдельный модуль для каждого этапа. Каждый этап разрабатывался и тестировался изолированно. Затем я объединил модули в единое целое и наладил их работу с помощью соответствующей функции Flow.
Высокоуровневый поток
В своём проекте разработки системы рекомендаций я использовал следующий непрерывный процесс:
Создание списка рекомендованных ресурсов (подходящих книг, демо-роликов, сайтов, обучающих курсов) вручную.
Определение набора ключевых слов:
"научиться", "книга", "книги", "электронные книги", "видео", "учебное пособие", "программирование на Elixir" и т. д.
Поиск по ключевому слову на форуме об Elixir и получение списка тем.
Выборка постов по данной теме:
парсинг содержимого поста (html-страницы);
удаление тегов <aside/> и <code/>;
разделение текста на предложения тегом <p>;
извлечение текста.
Поиск названий ресурсов в тексте.
Выделение положительных, отрицательных и нейтральных отзывов с помощью анализа тональности текста.
Объединение комментариев от одного и того же автора.
Оценка ресурсов и начисление им рейтинга согласно частоте их упоминания и характера отзывов с использованием сглаживания Лапласа.
Получаем следующий код на Elixir (при участии Flow):
Можно заметить, что на некоторых этапах конвейера присутствуют дополнительные функции Flow.partition. Дополнительное разбиение позволяет убедиться в том, что данные передаются в тот же процесс, и минимизировать количество передаваемых сообщений. Для секционирования данных используется хэш-функция. Можно провести секционирование по функции, или по ключу-кортежу, это также необходимо при объединении или разделении каких-либо данных. При секционировании данные в каждой секции не будут накладываться друг на друга.
Форум об Elixir построен на платформе Discourse с общедоступным интерфейсом, предоставляющим данные в формате JSON. Для этого достаточно будет просто добавить .json к запросу.
Я разработал простенький модуль ElixirForum, в котором для отправки HTTP-запросов использовал клиент-приложение HTTPoison, а для парсинга страницы – библиотеку Poison.
Чтобы поисковой робот работал надлежащим образом, на доступ к сайту с помощью ExRated поставлено ограничение по скорости так, что посылать можно лишь один запрос в секунду. Такое ограничение действует для всех запросов в данном модуле, независимо от вызывающего процесса, так как ExRated, как и GenServer, запускается с собственным состоянием.
Выглядит это так:
Отклики кэшируются на диск, чтобы повторные запросы не повлияли на работу сайта.
Модуль кэша HTTP сконфигурирован в супервизоре приложения.
Во время тестирования поиска я снова воспользовался библиотекой ExVCR для записи HTTP-запросов и откликов и для повторного запуска с диска последующих тестов.
Для парсинга HTML-кода и извлечения из него текста с помощью CSS-селекторов воспользуемся парсером Floki.
Абзацы определяются тегом <p>, а функция Floki.text/1 вытаскивает из них текст.
Парсинг предложений
Выделим отдельные предложения из текста поста. Для этого воспользуемся функцией Essence.Chunker.sentences из NLP-библиотеки essence.
Извлечение названий товаров
Чтобы извлечь названия отдельных товаров:
Разобьём предложение на отдельные слова, написанные строчными буквами, с помощью функции Essence.Tokenizer.tokenize:
“I recommend Elixir in Action” превратится в [“i, “recommend”, “elixir”, “in”, “action”]
Сделаем то же самое с названием товара:
“Elixir in Action” превратится в [“elixir”, “in”, “action”]
С помощью функции Enum.chunk произведём перебор расчленённых предложений, передав в неё длину искомого слова и шаг равный единице для учёта перекрытий, и осуществим поиск по заданному имени товара.
Анализ тональности
Для несложных анализов тональности я обычно использую систему Sentient. Она работает на основе списка слов AFINN-111, разделяя предложения на положительные, отрицательные и нейтральные на основе присутствия этих слов в тексте и их эмоциональной окраски.
AFINN – это список английских слов, отмеченных целыми числами согласно их коннотациям от минус пяти (отрицательная коннотация) до плюс пяти (положительная коннотация).
К примеру, текст "я рекомендую книгу "Elixir in Action" к прочтению" получит положительную оценку, поскольку слово "рекомендовать" отмечено значением числом +2. Список AFINN-111 содержит 2477 слов. Этот простейший алгоритм вполне адекватно оценивает тональность предложений.
Оценка тональности предложения, содержащего название товара, добавляется к общей оценке этого товара.
Рекомендации (оценки)
Общая оценка определяется заново каждый раз, когда в тексте было найдено название того или иного товара и была проведена оценка тональности.
Чтобы при оценке отделить соотношение положительных/отрицательных отзывов от малого количества отзывов по товару, воспользуемся формулой сглаживания Лапласа:
Например, при α = 1 и β = 2 товар без голосов получит оценку 0,5.
По каждому товару могут быть положительные, отрицательные и нейтральные отзывы. В моём случае каждый нейтральный комментарий получает оценку +1, а каждый положительный получает +2.
Существует исследование о применении сглаживания Лапласа для "предоставления наиболее верного решения проблемы ранжирования информации на основе оценок пользователей в веб-приложениях".
Объединяя отдельные этапы в единый конвейер с помощью Flow, можно оценить любой товар по отзывам на него.
Ecto сохраняет их в базу данных PostgreSQL. Отобразить товары, упорядоченные по рейтингу, поможет Ecto-запрос, представленный ниже. Товары можно отсортировать по языку программирования или по уровню знания языка и классифицировать их по этим признакам на сайте.
Контроллер Phoenix составляет запрос и осуществляет выборку подходящих ресурсов с помощью функции Repo.all/2:
Заключение
Рассмотренное приложение – пример использования Flow для построения вычислительного конвейера с параллельным выполнением операций для решения реальных задач. Предполагалось, что применение упрощённого алгоритма построения системы рекомендаций не имеет особого смысла. Однако, взглянув на конечные результаты, можно констатировать обратное: популярные ресурсы с высоким рейтингом занимают положение на вершине списка. Значение популярности, формирующееся из комментариев пользователей и результатов анализа тональности, может быть использовано для ранжирования товаров.
Недавно я добавил на сайт отслеживание активности пользователей, чтобы затем использовать эти значения для оценки. Таким образом, наиболее часто просматриваемые ресурсы имеют более высокий рейтинг. Возможно, в скором времени на сайте появится ещё и механизм обратной связи, чтобы пользователи могли отмечать ошибочные отзывы и оставлять свои.
Свяжитесь со мной, если у вас возникли какие-либо вопросы и предложения.
Оптимизация
Я был приятно удивлён алгоритмом оптимизации моего конвейера Flow, представленным Таймоном Тобольски, который написал две чудесные статьи на эту тему.
Взяв разработанный им модуль Progress и исходный код GnuPlot, я смог провести оптимизацию своего конвейера и визуализировать результаты его работы.
Отслеживание прогресса
Модуль Learn.Progress целиком взят из статьи Таймона.
Я также воспользовался библиотекой decorator Арджана Шерпениссе, чтобы добавить отслеживание прогресса в функцию каждого этапа конвейера, поставив перед объявлением каждой функции @decorate progress.
Декоратор увеличивает счётчик метода после его выполнения. Счётчик увеличивается на количество элементов в возвращаемом функцией списке или на единицу, если список не был возвращён.
Функция Learn.Indexer.rank/3 передаёт в процесс имя каждого этапа перед выполнением потока. После этого она останавливает процесс, проверяет, чтобы файлы журналов были созданы и закрыты.
Визуализация потока
После запуска индексатора, в котором функции этапов были отмечены тегом @progress, я построил графики "до" и "после".
Изначальный поток
В первоначальной версии приложения поток запускался в течение 33 секунд. HTTP-кэш заполнен, внешние запросы отсутствуют.
Поток после оптимизации
Я изменил опции max_demand и stages для некоторых этапов Flow, как показано выше. Это позволило сократить время запуска потока с 33 секунд до 26.
Графики были построены с помощью следующего кода (GnuPlot): Они показывают прогресс выполнения каждого этапа в процентах. Полученные рекомендации, упорядоченные по рейтингу, показаны на отдельной оси y.
Один-два раза в неделю присылаем тёплые письма об Эликсире: переводы самых интересных статей до их появления в открытом доступе, анонсы событий и вкусные бонусы.
Обязательно подтверди почту, перейдя по ссылке в письме, иначе мы не сможем делиться с тобой полезностями.