逃げる8回で会心の一撃

Web エンジニアのサトウリョウスケが開発とか色々書くブログです

MAU, DAU も計測できるようになった redis-objects-daily-counter v0.3.0

前回のブログで redis-objects-daily-counter という gem を作ったという話をしました。

ryz310.hateblo.jp

今回は v0.3.0 をリリースしたのでそちらのご紹介です。

【おさらい】redis-objects-daily-counter はどういう gem なのか

redis-objects gem の拡張として作られており、日付の変更毎に新しい counter を定義してくれる daily_counter 機能を提供する gem です。

例えば以下のような Homepage という class で daily_counter :pv という定義をします。

class Homepage
  include Redis::Objects

  daily_counter :pv, expireat: -> { Time.now + 2_678_400 } # about a month

  def id
    1
  end
end

すると以下のように #pv というインスタンスメソッドが使えるようになります。 これは redis-objects gem の counter の拡張であり、 #increment#decrement というメソッドで Redis 上の値を更新することが可能です。

# 2021-04-01
homepage = Homepage.new
homepage.id # 1

homepage.pv.increment
homepage.pv.increment
homepage.pv.increment
puts homepage.pv.value # 3

daily_countercounter とどのように違うかというと、日付が変わるとRedis 上の Key が変化するという点です。

# 2021-04-02 (next day)
puts homepage.pv.value # 0
homepage.pv.increment
homepage.pv.increment
puts homepage.pv.value # 2

これは、 daily_counter が以下のようなルールに則って Redis の Key にアクセスするように振る舞うためです。

model_name:id:field_name:yyyy-mm-dd

なお、この時のタイムゾーンは実行中の Ruby プロセスの標準タイムゾーンに従いますが、 Rails 上で実行した場合は application.rb で指定したタイムゾーンに従います。

日付が変わった後も、前日までの Key-Value は削除していないので、 #range#[]#at というメソッドでアクセスすることが可能です。 ちなみに #range#[] では値が返却されますが、 #at では Redis::Counter インスタンスが返却されます。 v0.2.0 までは #[]#at は同じ挙動でしたが、 v0.3.0 からそういう実装に変わりました 🙏

# 2021-04-01
homepage.pv.increment(3)

# 2021-04-02 (next day)
homepage.pv.increment(2)

# 2021-04-03 (next day)
homepage.pv.increment(5)

homepage.pv[Date.new(2021, 4, 1)] # => 3
homepage.pv[Date.new(2021, 4, 1), 3] # => [3, 2, 5]
homepage.pv[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => [3, 2]

homepage.pv.delete_at(Date.new(2021, 4, 1))
homepage.pv.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => [0, 2, 5]
homepage.pv.at(Date.new(2021, 4, 2)) # => #<Redis::Counter key="homepage:1:pv:2021-04-02">
homepage.pv.at(Date.new(2021, 4, 2)).value # 2

v0.3.0 で追加された機能とは

daily_counter に加えて daily_set が追加されました。 こちらは redis-objects gem の setdaily- 機能を追加したものになります。

class Homepage
  include Redis::Objects

  daily_set :dau, expireat: -> { Time.now + 2_678_400 } # about a month

  def id
    1
  end
end

homepage.dau << 'user1'
homepage.dau << 'user2'
homepage.dau << 'user1' # dup ignored
puts homepage.dau.members # ['user1', 'user2']
puts homepage.dau.length # 2
puts homepage.dau.count # alias of #length

set は重複を受け付けない Array と考えて良いと思います。 上記の例を見てもらうと分かりますが user1 を 2 回追加しても #members による取得結果には 1 つしか含まれていません。

これを使って何が実現できるかというと、 DAU (Daily Active User) の計測です。 ログインしたユーザーの ID を daily_set で定義した変数に追加していくことで、その日アクティブだったユーザー一覧とその数を知ることが出来ます。

# 2021-04-01
homepage.dau.merge('user1', 'user2')
puts homepage.dau.members # ['user1', 'user2']

# 2021-04-02 (next day)
puts homepage.dau.members # []

homepage.dau.merge('user2', 'user3')
puts homepage.dau.members # ['user2', 'user3']

# 2021-04-03 (next day)
homepage.dau.merge('user4')

homepage.dau.at(Date.new(2021, 4, 2)) # => #<Redis::Set key="homepage:1:dau:2021-04-02">
homepage.dau.at(Date.new(2021, 4, 2)).members # => ['user2', 'user3']
homepage.dau.at(Date.new(2021, 4, 2)).length # => 2

#range#[] を使う事で指定した期間内の値を結合することも出来ます。 これによって DAU 目的で記録した値から MAU を求めることも可能です。

homepage.dau[Date.new(2021, 4, 1)] # => ['user1', 'user2']
homepage.dau[Date.new(2021, 4, 1), 3] # => ['user1', 'user2', 'user3', 'user4']
homepage.dau[Date.new(2021, 4, 1)..Date.new(2021, 4, 2)] # => ['user1', 'user2', 'user3']

homepage.dau.delete_at(Date.new(2021, 4, 1))
homepage.dau.range(Date.new(2021, 4, 1), Date.new(2021, 4, 3)) # => ['user2', 'user3', 'user4']

#at で一日ずつ値を取得して RubyArray#uniq を使っても同じ結果が得られますが、 #range#[] の方が内部で Redis の SUNION を利用するため高速だと思います。

ちなみに #range#[] の結果は Ruby の Array なので、 #length を使って長さを求める際は実行中のマシンリソースを消費します。 一方 #at では Redis::Set を返しており、 Redis::Set#length は内部で Redis の SCARD を実行するため、 #length#at からの方が高速だと思います。 ただし一定期間の値の結合は #range#[] を使う必要があるので、結局マシンリソースの消費は避けられないですね。。

この辺りの仕様は実装していてかなり頭を悩ませました。なので今後のアップデートで破壊的な変更が入るかも知れません。

monthly_set なども実装してあります

前回の記事でも紹介したような monthly_counter に相当する monthly_set なども使えます。 MAU, WAU, DAU で使い分けると良いかと思います。

  • annual_set
    • Key format: model_name:id:field_name:yyyy
    • Redis is a highly volatile key-value store, so I don't recommend using it.
  • monthly_set
    • Key format: model_name:id:field_name:yyyy-mm
  • weekly_set
    • Key format: model_name:id:field_name:yyyyWw
  • daily_set
    • Key format: model_name:id:field_name:yyyy-mm-dd
  • hourly_set
    • Key format: model_name:id:field_name:yyyy-mm-ddThh
  • minutely_set
    • Key format: model_name:id:field_name:yyyy-mm-ddThh:mi