Некоторое время назад на Хабре была опубликована статья про поиск похожих аккаунтов в Twitter'e. На комментарии автор, к сожалению, не реагировал, потому пришлось изобретать велосипед. Но чтобы не делать уж совсем то же самое, было решено искать похожие аккаунты в Instagram с помощью Google App Engine, да так, чтобы воспользоваться сервисом мог каждый. Так появился instalytics.ru*.
Самое сложное, конечно же, оказалось в том, чтобы реализовать сервис для всех (ну, и остаться в пределах бесплатных квот Google App Engine и учесть ограничения Instagram API).
Реализовано все следующим образом —
- Пользовательский запрос на анализ аккаунта проверяется в Instagram — если заданный пользователь найден, то запрос на его анализ добавляется в базу данных. При этом у каждого запроса есть свой приоритет (пока у всех запросов одинаковый).
- Раз в 15 минут с помощью cron'a запускается задача, которая выбирает из базы данных один запрос из очереди и создает новую задачу — получить всех подписчиков пользователя из запроса. Задача в случае ошибкир повторяется еще:
- name: followers-get rate: 1/m # 1 task per minute bucket_size: 1 max_concurrent_requests: 1 retry_parameters: task_retry_limit: 2 min_backoff_seconds: 30
Каждая задача, в случае, если за один запрос были получены не все подписчики, создает новую задачу:
if users and users.get('pagination') and users.get('pagination').get('next_cursor'): cursor = users.get('pagination').get('next_cursor') url = '/task/followers-get?user_id='+user_id url += '&cursor=' + cursor taskqueue.add(queue_name='followers-get', url=url, method='GET')
- После завершения получения всех подписчиков начинается анализ каждого. Для этого создается огромное количество задач на получение списка тех пользователей, на кого подписан каждый подписчик (каждая задача при этом может создавать новые задачи, как и в случае с подписчиками выше). Для того, чтобы быть в соответствии с лимитом Instagram на 5'000 запросов в час, очередь задач настроена следующим образом:
- name: subscriptions-get rate: 5000/h
При этом после выполнения каждого запроса, на всякий случай, спим по 0,72 секунды (=60*60/5000).
К сожалению, в бесплатной версии Google App Engine можно осуществить только 50'000 операций записей в базу данных в сутки. Т.к. каждая задача может создать новую задачу, то изначальный вариант — записывать результат выполнения каждой задачи в базу данных — пришлось заменить на новый — результат выполнения предыдущей задачи передается как параметр новой задачи, и лишь последняя задача записывает результат в базу:if users and users.get('pagination') and users.get('pagination').get('next_cursor'): cursor = users.get('pagination').get('next_cursor') params = { 'user_id': user_id, 'f_user_id': f_user_id, 'cursor': cursor } if more_subscriptions: params['subscriptions'] = ','.join(more_subscriptions) taskqueue.add(queue_name='subscriptions-get', url='/task/subscriptions-get', params=params, method='POST')
Некоторые пользователи (такие, как @instagram, например) имеют миллионы подписчиков. Дабы не тратить драгоценные ресурсы на получение всех их подписчиков, задача завершается после получения 100'000 подписчиков.
- Из-за ограничения на количество операций записей в базу данных, не удается нормально отслеживать завершились ли все задачи по конкретному пользователю или нет. Нормальным решением было бы записывать в базу данных список id запущенных задач и по завершении каждой задачи (или если сделана последняя попытка выполнить задачу) исключать задачу из списка. Но огромное количество задач помноженное на всех пользователей не позволяет этого сделать. Потому список задач хранится в memcache:
memcache.set('subscriptions'+str(user_id), ','.join(str(x) for x in followers), 1209600)
Данные из memcache могут быть удалены в любой момент. Чтобы избежать ситуации с «зависшим» запросом (когда все задачи по запросу были выполнены, но memcache был удален и мы об этом, соответственно, не знаем), раз в несколько часов запускается задача, которая проверяет нет ли запросов, получивших статус получения подписчиков более чем 2 недели назад (пока считается, что это то время, за которое точно будут завершены все задачи). Если такие запросы находятся, то они «силой» переводятся на следующий этап.
- На следующем этапе считываются все полученные ранее данные из базы данных. Как оказалось, данных может быть довольно много и выделенной GAE оперативной памяти для них может не хватить. Потому данные считываются порциями, для каждой порции рассчитывается промежуточный результат, который потом добавляется к следующему промежуточному результату. В этом процессе пришлось отключить автоматические кэши:
ctx = ndb.get_context() ctx.set_cache_policy(lambda key: False) ctx.set_memcache_policy(lambda key: False)
В результате многочисленных вычислений на этом этапе выбираются 300 наиболее популярных пользователей, на которых подписаны ваши пользователи.
- Для каждого из 300 пользователей запускаются задачи по получению данных по ним (имена, картинки, количество подписчиков и т.п.). По аналогии с описанным выше процессом, ожидается либо завершение всех задач, либо принудительно через некоторое время запускается новый этап.
- На последнем этапе осуществляется расчет и выбор наиболее похожих пользователей (с учетом количества ваших подписчиков и подписчиков всего). Получается что-то типа этого, ссылка на результат отправляется на e-mail.
Указанные выше подходы и оптимизации пока позволяют оставаться в рамках выделенных GAE бесплатных квот, хотя получение результата и занимает достаточно много времени. Нужна ваша помощь — добавляйте своих пользователей в очередь, посмотрим сколько времени займет их анализ.
В будущем планирую добавить к сервису распознавание настоящих людей / компаний в Instagram, но без машинного обучения тут не обойтись — так что это будет отдельной задачей.
* Русский язык на сайте пока не работает — не могу разобраться, почему django translation не работает на GAE.
Автор: and7ey