Leaderboard ranking via elasticsearch (with tire)

1k Views Asked by At

I have a mapping that boils down to the following (unrelated fields removed):

mapping 
  indexes :id, type: 'integer', index: :not_analyze        
  indexes :first_name, boost: 5, type: 'string', analyzer: 'snowball'
  indexes :votes, type: 'integer', index: :not_analyzed
end

At the moment I'm calculating ranking via postgres, so that given the following entries:

| first_name | votes |
----------------------
| Andy       |     5 |
| Barry      |     8 |
| Carl       |     5 |
| Derek      |     1 |

Using postgres, I can get the following:

| first_name | votes | rank |
-----------------------------
| Barry      |     8 |    1 |
| Andy       |     5 |    2 |
| Carl       |     5 |    2 |
| Derek      |     1 |    4 |

Is it possible somehow calculate this ranking via elasticsearch?

2

There are 2 best solutions below

0
On

Redis is indeed an ideal solution for leaderboards. While it is introducing another technology, if you're using AWS, note that ElastiCache managed Redis was just launched this week.

Generic Redis commands would be:

zadd votes 5 "Andy"
zadd votes 8 "Barry"
zadd votes 5 "Carl"
zadd votes 1 "Derek"

Then to get the leaderboard with most votes as highest ranked:

zrevrange votes 0 -1

Refer to the Redis docs for ZREVRANGE for more details.

For Ruby on Rails, I'd encourage you to look at my redis-objects gem which is popular since it easily integrates with ActiveRecord. Assuming you have a table with a votes column as shown, you could update the ranking on save:

class User < ActiveRecord::Base
  include Redis::Objects
  sorted_set :rank, global: true

  after_save :update_rank
  def update_rank
    self.class.rank[id] = votes
  end
end

Then retrieve the leaderboard:

User.rank.revrange(0, -1)

In this example, this will return id values which you could then use to retrieve records as follows. (You could also store first_name or some other unique value.)

ids = User.rank.revrange(0, -1)
users = User.where(id: ids).all

You can paginate through results with revrange by passing different start/end values:

User.rank.revrange(0, 9)
User.rank.revrange(10, 19)

You could easily wrap this in a self. method in User that retrieved a page of rankings from Redis and returned the DB records accordingly.

1
On

I don't believe ElasticSearch is the place to do it, since updating a single document would require recalculation of all ranking values. Not possible, as far as I can tell.

Instead, once you get the results you can use Ruby to calculate the ranking with something like this:

scores = {:a=>5, :b=>8, :c=>5, :d=>1}
scores.values.sort{|a,b| a <=> b}.tap do |sorted_scores|
  sorted_scores.each{|vote| puts sorted_scores.index(vote)+1 }
end