SQL
1create table `users` ( 2`user_id` int auto_increment primary key, 3`active` tinyint 4)engine=innoDB; 5 6create table `articles` ( 7`article_id` int auto_increment primary key, 8`user_id` int, 9`created_at` datetime 10)engine=innoDB; 11 12create table `article_metadata_ref` ( 13`metadata_id` int, 14`article_id` int, 15Primary Key(`metadata_id`, `article_id`) 16);
説明用に最小サンプルを作成しました。
PHP PDO からデータベースに繋いでいます。
user テーブルのactive フラグか articles テーブルの created_at の日付をみて活動していると判断できるユーザーのアイテムn個に紐付いているmetadata_id とそのcount(*) をユーザー毎に取得するのが目的です。
自分でも作成はしたのですがかなり遅いので最適化して少しでも早くしたいと思いますので解決の糸口でも頂ければ幸いです。
試した事
まずは正直にでユーザーIDのリストを取得してから
SQL
1select `user_id` from users where `active` = 1;
各ユーザーのarticle_id をn個 取得してref table とjoin
SQL
1select `metadata_id`, count(*) from 2 (select `article_id` 3 from articles where `user_id` = :user_id order by created_at desc limit :n 4 ) as articles 5 inner join article_metadata_ref as ref on articles.`article_id` = ref.`article_id` 6group by `metadata_id` 7 8結果: 9+-------------+----------+ 10| metadata_id | count(*) | 11+-------------+----------+ 12| 1 | 1 | 13| 2 | 1 | 14+-------------+----------+ 152 rows in set (0.00 sec)
とこの様な形でユーザーのmetadata_idと使用頻度を取得しました。
上記のクエリで取得したmetadata_idを他のユーザーと比較したい場合はユーザーの数だけクエリを発行する必要があってユーザー数が多いと大変時間がかかります。
なので他のユーザーのmetadata_id を同時に取得しようと思って下記のクエリを考えてみました。
SQL
1select `metadata_id`, count(*) from 2 (select `article_id`, `user_id` 3 from articles where `user_id` IN (:uid1, :uid2, ...) order by created_at desc limit :(n*ユーザー数) 4 ) as articles 5 inner join article_metadata_ref as ref on articles.`article_id` = ref.`article_id` 6group by `metadata_id`,articles.`user_id` 7 8結果: 9+---------+-------------+----------+ 10| user_id | metadata_id | count(*) | 11+---------+-------------+----------+ 12| 1 | 1 | 1 | 13| 2 | 1 | 1 | 14| 1 | 2 | 1 | 15| 2 | 2 | 1 | 16| 1 | 3 | 1 | 17| 1 | 4 | 1 | 18| 1 | 5 | 1 | 19+---------+-------------+----------+ 207 rows in set (0.00 sec)
ただこちらのクエリだと limit n * ユーザー数ですが 特定ユーザーのarticle数が均等では無いのでデータに偏りが出る可能性がありますので使えないと思いました。
その他にrow_number() を使って取得出来ると読みましたがmysql のバージョンが5.6 なので使えないのと変数を使って実現してもユーザーの全てのarticle のレコードにrow_number を付けてからwhere でフィルタするので結果遅くなりました。
リアルタイムでmetadata_id によるユーザー間の類似度のスコアリングはスピード的に無理そうなのでcronで先に計算したテーブルを用意しようと思いましたがそれでも数万ユーザー分の計算だと大変遅いので良い方法は無いでしょうか。
直接の解決策ではなくてもアドバイスなどコメントご教示頂ければ幸いです。
またこの様なレコメンドの実装に関してもご意見頂ければと思います。調べるとhashを使ったの高速の類似度の近似値の求め方などが出てきますがそもそもデータを用意するのが遅すぎてその様な計算の高速化以前の問題で止まっています。