DailyWatchでは、私たちの「関連動画」パネルは、ささやかなSQLiteクエリから始まりました。3つのテーブルを結合し、重複するタグをカウントして、ランク付けされたリストを返していました。小規模なカタログであれば、それで十分でした。「italian」や「pasta」というタグが付いた料理動画は、同じタグを持つ他の動画を表示し、ユーザーはそれをクリックしました。メタデータがクリーンでライブラリが浅かったため、出力は関連性があるように見えました。

しかし、カタログが成長し、視聴者の期待が変化しました。人々は単に同じタグを持つ動画を求めているわけではありませんでした。彼らが求めていたのは、現在の動画の直後に視聴者の40%が視聴したクリップです。説明文に開始動画に関する記述がなく、タグも乏しいにもかかわらず、深夜の視聴セッションで繰り返し現れるニッチなチャンネルです。これらはメタデータの照合ではなく、行動パターンなのです。SQLiteでは、自己結合や再帰的共通テーブル式(CTE)の維持不可能な入れ子構造に陥ることなく、これらを表現することはできませんでした。共視聴(co-viewership)やセッションの隣接性など、モデル化しようとする新しいシグナルを追加するたびに、レイテンシと精神的なオーバーヘッドが増大しました。私たちは、リレーショナルな構造に押し込められたグラフ問題に直面していたのです。

私はレコメンデーション層をApache AGEに移行しました。

Apache AGEは、すでに運用しているデータベースにopenCypherグラフクエリを追加するPostgreSQL拡張機能です。これは独立したサーバーではありません。サイドカーでもありません。Postgres内で動作するため、専用のNeo4jクラスターを立ち上げたり、全く新しい運用手順を学んだりすることなく、共視聴ネットワークを構築できました。データベース信頼性エンジニア(DBRE)がいない小規模なチームにとって、この違いは極めて重要でした。

なぜ新しいデータベースよりも拡張機能の方が優れているのか

スタックにグラフデータベースを追加することは、ホワイトボードの上では簡単ですが、本番環境ではコストがかかります。新しいモニタリングダッシュボード、新しいバックアップ手順、新しいコネクションプール、そして新しいフェイルオーバーロジックが必要になります。AGEは既存のPostgresインスタンス内で動作するため、これらすべてを回避できます。

これが私たちにとってうまく機能した実用的な理由が4つあります。

  • 新しいインフラが不要: AGEは拡張機能であるため、現在の pg_dump スケジュール、既存のレプリカ、標準的なPostgresのヘルスチェックがすべてそのまま機能します。運用担当者に別のデータストアの世話をさせる必要はありません。
  • 1つのクエリで混合ワークロードを実現: AGEを使用すると、SQL内でCypherを書くことができます。グラフ探索を実行して候補となる動画を見つけ、その結果セットをリレーショナルな users テーブルと結合して地域的なコンテンツ制限を適用したり、リレーショナルな sponsorships テーブルと結合して特定のチャンネルの優先度を下げたりすることができます。1回のラウンドトリップで、2つのクエリ言語が協調します。
  • ポータビリティ: Cypherはオープンで、よく文書化されたグラフクエリ言語です。もしDailyWatchがAGEの規模を超え、後にNeo4jやMemgraphへの移行が必要になったとしても、クエリロジックは最小限の書き換えで移行できます。独自のプロプライエタリな方言にロックインされることはありません。
  • 互換性: データは最終的にPostgreSQLに格納されるため、既存のPHPやPythonのツールを変更する必要はありません。同じドライバーで接続し、同じ接続文字列を扱い、同じ方法で行を取得できます。グラフロジックはアプリケーション層ではなく、クエリ層に存在します。

共視聴のモデリング

実装は簡単です。関心のあるエンティティに対して VideoChannel というノードを定義しました。次に、それらの間の関係に対してエッジを定義しました。PUBLISHED エッジは ChannelVideo にリンクします。CO_VIEWED エッジは1つの Video を別の Video にリンクし、2つの動画が同じ視聴セッションにどの程度の頻度で現れたかを表す weight プロパティを保持します。

このモデルは、タグベースのSQLでは捉えられないもの、すなわち暗黙的な構造を捉えます: