見出し画像

ランキング設計はどうあるべきか? その3

ここまでランキングのあるべき方向性と、実行可能なアプローチについて考察してきた。そして、いよいよプロトタイピングと実験の時間だ。残念ながら自分はサーバーサイドのコードが書けないので、ここからは開発チームに託すことになる。

妄想や実証不能なものをオーダーするのは非効率だと思う。ある程度はクラスをモデリングしておくと、エンジニアとディスカッションしやすい(ように思える)。

とりあえずnoteでのランキングは、様々な試行錯誤や実験が予想される。そのため、以下のような要素が必須となる。

・工数最小
・あらゆるランキングを表現できる
・拡張しやすい

今回はDecoratorパターンとCommandパターンを混ぜたような実装で、柔軟性のあるランキング計算システムのコンセプトを描いてみた。下手なコードでも、設計がある方がエンジニアさんに説明しやすい。

設計イメージとしては、まずランキングの各処理を同じインターフェースのクラスで記述する。次に、シェーダーのレンダリング・パイプラインのように、このクラス群を接続しランキングを計算する。

このように設計すれば、クラスの組み合わせや順番を変えるだけで、あらゆるランキングのアルゴリズムが記述できるはずである。


ライブラリのインターフェースを設計する

インターフェースとしてIRankingGeneratorを作る。関数getRankingを呼ぶと、エントリを配列形式で返すだけの極めてシンプルな実装。

interface IRankingGenerator{
 ArrayList<Entry> getRanking();
}

実際はバッチ処理で長時間かかるわけだけど、コンセプト的にはこんな感じ。


ライブラリのベースクラスを設計する

あらゆるランキングの基底クラスとなる、RankingGeneratorクラスを作る。中身はシンプルにインターフェースを実装するだけ。

//あらゆるランキング計算の基底クラス
class RankingGenerator implements IRankingGenerator{
 ArrayList<Entry> getRanking(){
  //サブクラスで実装。ランキング計算 or キャッシュから取得し返す。
 }
}


はてな互換のランキングを設計する

例えば、はてな互換のランキングを作る場合、以下のように基底クラスを拡張して作る。

//はてな的なランキング計算を行うクラス
class HatenaRankingGenerator extends RankingGenerator{
 ArrayList<Entry> getRanking(){
  //はてなの時間減衰アルゴリズムでランキングを返すコード。
 }
}

実際にトップ100の単純ランキングを計算する場合は、以下みたいに書く。極めてシンプル。

RankingGenerator ranking = new HatenaRankingGenerator(100);
ArrayList entries = ranking.getRanking();

これで、あらゆるランキングを同一の作法で作ることができるようになった。RankingGeneratorクラスを拡張して、PVランキング、読了率ランキング、カテゴリランキングなど色々なものを、同一視して作ることができる。


ランダムピッカーを作る

さらに取得したランキングに対して、後加工でシャッフルをしたいとする。このとき、配列を直接弄ってシャッフルをするとコードの再利用性が下がる。そこで、ランダムピックも同じインターフェースでクラス化する。

class RandomRanking extends RankingGenerator{
 IRankingGenerator generator;
  
 RandomRanking(IRankingGenerator generator){
  this.generator = generator;
 }

 ArrayList<Entry> getRanking(){
  ArrayList<Entry> list = generator.getRanking();
  //ここでランキングをシャッフル
  return list    
 }
}

ランキングをシャッフルするクラスを、このようにRankingGeneratorのサブクラスとすることで、複数のランキングをネストして扱うことができる。コード的には以下のようにかけるわけだ。

//はてなランキングを算出し、その中からランダムに50個を抽出
RankingGenerator ranking = new HatenaRankingGenerator(100);
RandomRanking ranking2 = new RandomRanking(ranking, 50);
ArrayList entries = ranking2.getRanking();

こうすることで、「はてな的ランキング」を作るクラスと、「ランキングをランダムにシャッフルするクラス」を分離できる。

サブクラスを増やせば、「はてな的ランキングを作成」→「ランダムに30個をピックアップ」→「読了率順にソート」など、複数の処理の組み合わせを、インスタンスのネストで記述できるようになる。

他にも、「順番を逆にする」「シャッフルする」「特定の条件でソートする」などの後処理系のサブクラスを作っておくと良い。


ランキングのブレンドを実装する

複数のランキングのブレンドも、同一のインターフェースで実装できる。2つ以上のランキング計算クラスを内包し、隠蔽してしまえば良い。

このような設計をすることで、複数のランキングのブレンドも、同一の手続きで行える。

//新人だけのランキング
RankingGenerator youngRank = new YougRankingGenerator();
//常連だけのランキング
RankingGenerator oldRank = new OldRankingGenerator();

//2つのランキングをブレンダーで混ぜる
RankingGenerator blendRanking = new BlendRankingGenerator(youngRank, oldRank);

ArrayList entries = blendRanking.getRanking();


この辺りまで作れば、最低限コンセプト図で書いたような処理が、自由に構築できる。

個人的にネスト大好きマンなので、ネスト構造にしたが、リスト構造にしてレンダリング・パイプラインのような設計にしても良いかもしれない。

最終的には、「ランキング対象を決定」と「評価方法を決定」を、それぞれクラス化して、内包していけばさらに抽象化していくことができると思う。


チューニング

理想形としては、複数のアルゴリズムのチューニングを、遺伝やバンディット・アルゴリズムなどで自動チューニングしたい。が、スピード優先なので、まずは手動と目算で見当をつけながら、数十種類のランキングをテストしていきたい(ここは、それだけの価値がある)。

ざっくりした雑設計だが、あとはきっとCTOがなんとかしてくれるはず。


いただいたサポートは、コロナでオフィスいけてないので、コロナあけにnoteチームにピザおごったり、サービス設計の参考書籍代にします。