Now that our users can follow one another, we can run some more interesting queries (aka traversals) on our data. For example, we may want to discover the followers of followers of a given user.
xget 'model/user/id/73/rel/followed_by/rel/followed_by'
And see what they are tweeting about.
xget 'model/user/id/73/rel/followed_by/rel/followed_by/rel/tweets'
Another example would be to find users with similar taste (i.e. users that follow the same people).
xget 'model/user/id/73/rel/follows/rel/followed_by'
The result of the above query may contain duplicates. For example, if users 73 and 75 follow the same three people, user 75 will appear in the result three times. If we are not interested in duplicates, there is an easy fix.
xget 'model/user/id/73/rel/follows/rel/followed_by/unique'
But there is another problem with this query - If the result is not empty, it will include user 73. This is not what we want! We want to find other users with similar taste.
At this point, you might be wondering … Is there a quick fix for our query? Is the API flexible enough to cover any crazy query I come up with? If the API is flexible enough, will I have to learn a gazillion commands in order to write my queries?
We asked ourselves the same questions, when we built the xnlogic framework. These questions led us to the following approach:
This design allows us to make the query above accessible via the following API call:
xget 'model/user/id/73/to/similar_users'
Let’s see how this is done …
Custom queries are defined using route traversals. The concept of route traversals is somewhat similar to actions - Actions expose instance methods to the API, while route traversals expose custom queries.
Just like with actions, defining route traversals is a two-step process:
Route
module.route_traversal
declaration.In order to define our similar_users
traversal, we will need to add the following code to the User
module in lib/my_app/parts/user.rb
.
route_traversal :similar_users, return_parts: :User do
self.similar_users.uniq
end
module Route
def similar_users()
self.follows.followed_by.except(self)
end
end
There are a few things we should note about the code above:
similar_users
method inside the Route
module.:similar_users
).return_parts
attribute.uniq
filter in the route traversal’s body, and not in the instance method. This will become useful later in this section.Before we start accessing our route traversal, let’s create some fake data, in order to make the results more interesting.
Let’s create some fake users (using the API) by pasting the following code in the console.
xput '/model/user', {username: 'grover_kiehn', email: 'jee_smith@hotmail.com'}
xput '/model/user', {username: 'maxine.wehner', email: 'demarcus@yahoo.com'}
xput '/model/user', {username: 'carolina_marquardt', email: 'maida_parisian@gmail.com'}
xput '/model/user', {username: 'golden.kunde', email: 'valentina@hotmail.com'}
xput '/model/user', {username: 'belle', email: 'bonnie@gmail.com'}
xput '/model/user', {username: 'mateo_hirthe', email: 'hettie_schaden@hotmail.com'}
xput '/model/user', {username: 'elia_keler', email: 'effie@hotmail.com'}
xput '/model/user', {username: 'kaitlyn.corwin', email: 'elton@gmail.com'}
xput '/model/user', {username: 'grayson', email: 'derick_huels@hotmail.com'}
xput '/model/user', {username: 'loren', email: 'taya@hotmail.com'}
xput '/model/user', {username: 'winfield', email: 'haven@yahoo.com'}
xput '/model/user', {username: 'keira.hills', email: 'fern@gmail.com'}
xput '/model/user', {username: 'gregoria', email: 'caesar@hotmail.com'}
xput '/model/user', {username: 'anika', email: 'zelda.graham@gmail.com'}
xput '/model/user', {username: 'lenora', email: 'hugh.barton@yahoo.com'}
xput '/model/user', {username: 'oswald_schuster', email: 'jey_beatty@hotmail.com'}
Next, let’s create some random follow-relations between our users (using Ruby code) by pasting the following code in the IRB.
users = app.graph.v(MyApp::M::User).to_a
app.graph.transaction do
users.each do |user|
how_many = (rand * users.length).to_i
users_to_follow = users.shuffle[0...how_many].reject {|u| u == user}
user.add_follows(users_to_follow)
end
end
We access route traversals by appending /to/TRAVERSAL_NAME
to the URL path of a GET request. For example:
jruby-1.7.18 :033 > xget 'model/user/id/73/to/similar_users'
# ...
Total: 16
We can get specific properties from the result.
jruby-1.7.18 :034 > xget 'model/user/id/73/to/similar_users/properties/username'
We can also chain our traversal. For example, let’s see what users who are similar to user 73 are tweeting about.
jruby-1.7.18 :035 > xget 'model/user/id/73/to/similar_users/rel/tweets/properties/text'
Let’s see another, slightly more complicated, example of a route traversal - We would like to recommend a user other users he/she might be interested in following.
We will define the following method in the Route
module.
def recommended_users_to_follow(limit)
self.similar_users.follows.except(self).except(self.follows).most_frequent(0...limit)
end
Let’s mention a few things about the code:
similar_users
method keeps duplicates, users with “more similar taste to mine” will have more influence on the result of the recommendation results.At this point, we can expose our recommendation method as an API traversal, by adding the following code to the User
module.
route_traversal :should_follow, return_parts: :User do
self.recommended_users_to_follow(10)
end
We can reload our code, and use our traversal in order to get recommended users to follow.
jruby-1.7.18 :036 > xget 'model/user/id/73/to/should_follow'
As before, we can chain the traversal and see who these recommended users are following, and what these users are tweeting about.
jruby-1.7.18 :037 > xget 'model/user/id/73/to/should_follow/rel/follows/unique/rel/tweets/property/text'
Although the query above is fairly long, it shouldn’t scare you. After all, if you consider a query to be too long, you can always expose it as a route traversal with a meaningful name.
Note: Recommendation engines are one of the highlights of graph databases. The database allows us to traverse a neighbourhood of the graph efficiently, and Ruby allows us to easily express the traversal.
route_traversal
declaration. action
declarations.Route
module./to/TRAVERSAL_NAME
to the URL path.