Статьи

NeoSocial: подключение к Facebook с Neo4j

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

Приложение будет иметь два основных компонента:

  1. Веб-сервис, который обрабатывает аутентификацию и отображение друзей, лайков и так далее.
  2. Фоновая служба заданий, которая импортирует данные из Facebook.

Мы будем развертывать это приложение на Heroku и использовать дополнения RedisToGo и Neo4j .

Давайте начнем с клонирования приложения и его создания в Heroku.

git clone [email protected]:maxdemarzi/neosocial.git
heroku apps:create
heroku addons:add neo4j
heroku addons:add redistogo

 Поскольку мы подключаемся к Facebook, вам необходимо получить идентификатор приложения Facebook и секретный ключ по адресу https://developers.facebook.com/apps .

 

Включите «Веб-сайт с логином Facebook» и установите его в своем домене http://xxxxxxx.herokuapp.com.

Придумайте секрет сеанса (подойдет любой длинный текст или предложение) и добавьте его и ваши параметры Facebook в ваше приложение.

heroku config:add SESSION_SECRET=<your session secret>
heroku config:add FACEBOOK_APP_ID=<your facebook app id>
heroku config:add FACEBOOK_SECRET=<your facebook secret>

Теперь нам нужно просто развернуть наше приложение в Heroku и увеличить количество рабочих до 1.

git push heroku master
heroku ps:scale worker=1

Если вы перейдете на свой домен xxxxx.herokuapp.com, вы должны увидеть:

Так что же происходит, когда пользователь нажимает «Войти через Facebook»? Они отправляются в Facebook для аутентификации через Oauth, и, если они одобрят, создается объект User, и они отправляются на страницу своего профиля.

['get', 'post'].each do |method|
  send(method, "/auth/:provider/callback") do
    user = User.create_with_omniauth(env['omniauth.auth'])
    session[:uid] = user.uid

    redirect to(session[:redirect_url] || "/user/#{session[:uid]}")
    session[:redirect_url] = nil
  end
end

Давайте посмотрим на метод create_with_omniauth. Он создает уникальный узел с использованием идентификатора Facebook, токена и значений, которые мы получили при аутентификации, и возвращает нового пользователя.

def self.create_with_omniauth(auth)
    values = {"name"      => auth.info.name,
              "image_url" => auth.info.image,
              "location"  => auth.info.location,
              "uid"       => auth.uid,
              "token"     => auth.credentials.token}
    node = $neo_server.create_unique_node("user_index", "uid", auth.uid, values)

    Sidekiq::Client.enqueue(Job::ImportFacebookProfile, auth.uid)
    User.new(node)
  end

Узел — это просто хеш, и мы могли бы построить все это приложение, используя простые хэши, но это упрощает жизнь для создания реальных объектов и их использования. Вот наш класс User:

class User
  attr_reader :neo_id
  attr_accessor :uid, :name, :image_url, :location, :token

  def initialize(node)
    @neo_id     = node["self"].split('/').last.to_i
    @uid        = node["data"]["uid"]
    @name       = node["data"]["name"]
    @image_url  = node["data"]["img_url"]
    @location   = node["data"]["location"]
    @token      = node["data"]["token"]
  end
...
end

Использование реальных объектов позволяет нам связать некоторые методы, чтобы помочь нам. Например, клиент Facebook этого пользователя, который использует токен, который мы сохранили при аутентификации, и самоцвет Koala, чтобы дать нам одобренное соединение с Facebook.

def client
  @client ||= Koala::Facebook::API.new(self.token)
end

Давайте сделаем один шаг назад и посмотрим на линию раньше. Он использует гем Sidekiq для запуска фонового задания с именем ImportFacebookProfile.

module Job
  class ImportFacebookProfile
    include Sidekiq::Worker

    def perform(uid)
      user = User.find_by_uid(uid)
      ...

      # Import Friends
      friends = user.client.get_connections("me", "friends")
      friends.each do |friend|
        Sidekiq::Client.enqueue(Job::ImportFriends, uid, friend["id"])
        Job::ImportMutualFriends.perform_at(120, uid, friend["id"])
      end
    end

  end
end

Этот работник заводит друзей на Facebook и создает два набора рабочих мест. ImportFriends, который сразу же добавляется в очередь, который выполняет фактический импорт друга, и ImportMutualFriends, который добавляется в очередь через 2 минуты.

module Job
  class ImportFriends
    include Sidekiq::Worker

    def perform(uid, person_id)
      user = User.find_by_uid(uid)
      person = user.client.get_object(person_id)
      friend = User.create_from_facebook(person)

      # Make them friends
      commands = []
      commands << [:create_unique_relationship, "friends_index", "ids",  "#{uid}-#{person_id}", "friends", user.neo_id, friend.neo_id]
      commands << [:create_unique_relationship, "friends_index", "ids",  "#{person_id}-#{uid}", "friends", friend.neo_id, user.neo_id]
      batch_result = $neo_server.batch *commands
      ...

Задание ImportFriends извлекает полный профиль друга из Facebook и создает два «дружеских» отношения с пользователем (каждый идет в одну сторону). Задание ImportMutualFriends (полностью показано ниже) связывает друга с другими друзьями с помощью команды API Graph Graph для друга:

module Job
  class ImportMutualFriends
    include Sidekiq::Worker

    def perform(uid, person_id)
      user = User.find_by_uid(uid)
      person = user.client.get_object(person_id)
      friend = User.create_from_facebook(person)

      # Import mutual friends
      mutual_friends = user.client.get_connections("me", "mutualfriends/#{person_id}")

      commands = []

      # Make them friends
      mutual_friends.each do |mutual_friend|
        uid = mutual_friend["id"]

        node = User.find_by_uid(uid)
        unless node
          person = user.client.get_object(uid)
          node = User.create_from_facebook(person)
        end

        commands << [:create_unique_relationship, "friends_index", "ids",  "#{uid}-#{person_id}", "friends", node.neo_id, friend.neo_id]
        commands << [:create_unique_relationship, "friends_index", "ids",  "#{person_id}-#{uid}", "friends", friend.neo_id, node.neo_id]
      end
      batch_result = $neo_server.batch *commands
    end
  end
end

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

def friend_matrix
    cypher =  "START me = node({id})
               MATCH me -[:friends]-> friends -[:friends]-> fof
               WHERE fof <> me
               RETURN friends.name, collect(fof.name)
               ORDER BY COUNT(fof) "
    $neo_server.execute_query(cypher, {:id => @neo_id})["data"]
  end

У некоторых людей есть сотни друзей на Facebook, и наша визуализация выглядит не очень хорошо, когда мы пересекаем 50 друзей. Поэтому вместо того, чтобы визуализировать все связи, мы выберем случайную выборку из 20-50 друзей. Мы симулируем друзей, которые случайно появились на вашем дне рождения. Мы создадим объект JSON, который мы передадим в D3.js для визуализации для нас.

get '/visualization' do
    @user = current_user
    random_number = 20 + Random.rand(31)
    @user.friend_matrix.sample(random_number).map{|fm| {"name" => fm[0], "follows" => fm[1]} }.to_json
  end

Мы повторно используем визуализацию D3 Chord, которую мы видели ранее, и это все, что нужно.

Вы можете попробовать NeoSocial для себя на http://neosocial.herokuapp.com . Как всегда, полный пример приложения доступен на github .