Build Status

Hari

Mile-high view

Hari is a tool to abstract complex relationships between Ruby objects onto Redis data structures. It allows for expressive querying of those relationships as well, in an easy way. It is mostly geared towards typical social networking concepts like news feeds, activity logs, friends of friends, mutual friends, and so on.

Basic concepts

Hari embraces normal objects, and allows 2 major modes of operation: abstraction of Redis operations, and actual relationship creation and querying.

Abstraction of Redis operations (lists, sets, sorted sets, etc)

Imagine this User model class:

class User

  attr_reader :id

  def initialize(id)
    @id = id
  end

end

You can create a set to store the user relation with his friends:

user = User.new(20)

Hari(user).set(:friends_ids) << 10   # REDIS: SADD hari:user#20:friends_ids 10

Then it's possible, for instance, to query the mutual friends between users:

Hari(user: 20).set(:friends_ids) & Hari(user: 30).set(:friends_ids)

=> ["10", "25", "40"]

By now, you're probably wandering what the Hari() method does. It accepts an object like user as a parameter, or an identification of this object like "user#20", or even {user: 30} and returns a Hari::Node representation of the object referenced so you can call all operations available in Hari.

Lists, Sets and Sorted Sets operations are available in the Wiki.

Relationships

Hari uses the power of Redis data structures to create relations between objects, allowing you to traverse nodes and its relations like a graph.

# this gets the last 20 comments from the entities the user#1 follows
Hari(user: 1).out(:follow).out(:comments).limit(20)

Creating a relation can be as simple as:

Hari.relation! :follow, user, artist

To remove a relation, do:

Hari.remove_relation! :follow, user, artist

Let's mount some queries:

# artist followers
Hari(artist).in(:follow)

# entities that user follow
Hari(user).out(:follow)

# just the last 10 followers of artist
Hari(artist).in(:follow).limit(10)

# paginates from a score (timestamp.to_f),
# bringing the next 10 followers up from a last timestamp,
# (useful for pooling streams)
Hari(artist).in(:follow).limit(10).from(1375977470.382)

# paginates from a score down,
# bringing the previous 10 followers from a last timestamp
Hari(artist).in(:follow).limit(10).from(1375977470.382, :down)

# chaining relations between nodes
# last 10 entities to be followed by who user follows
Hari(user).out(:follow).out(:follow).limit(10)

# All users following artist
Hari(artist).in(:follow).type(:user)

All the calls above return a lazy query expression. The ruby code still didn't fetch the Redis backend, it's mounting a composable query.

Below there are some of the methods that make the query come to an end:

# how many followers
Hari(artist).in(:follow).count
=> 2001

# returns all followers nodes ids
Hari(artist).in(:follow).nodes_ids!
=> ['artist#20', 'user#30', 'user#33']


# returns all followers instances of Hari::Node
# depends that you have the nodes persisted for each object
# this is the default implementation when you do .to_a
Hari(artist).in(:follow).nodes!

tiesto_followers   = Hari(artist: 21).in(:follow).type(:user)
daftpunk_followers = Hari(artist: 42).in(:follow).type(:user)

# count of common users following two artists
tiesto_followers.intersect_count(daft_punk_followers)
=> 6

# actual users ids following two artists
tiesto_followers.intersect(daft_punk_followers)
=> [17, 29, 3, 173, 919, 11]

# paginating through them (start + stop)
tiesto_followers.intersect(daft_punk_followers, 2, 5)
=> [3, 173, 919]

user_friends = Hari(user).out(:follow).type(:user)

# bringing all followers of artist, but user's friends first
tiesto_followers.sort_by user_friends
=> [883, 317, 211, 157, 163, 103, 47, 53, 7]

# paginating through them (offset + count)
tiesto_followers.sort_by user_friends, 3, 5
=> [157, 163, 103, 47, 53]