個人開発のフィードでページ送りの仕組みを作り直した話

当ページのリンクには広告が含まれています。
この記事の結論
ページ送りの都合が悪いとき、その場をしのぐ作りを足すより、前提そのものを変えたほうが結果的に楽になると感じた話です。理由は、制約の多くが「並びは毎回変わる」という前提から出てきていたからです。作り直しの途中で、同じ性質の不具合が別の入り口に残っていることにも気づきました。

お疲れ様です!IT業界で働くアライグマです!

個人開発している開発関連ニュースのキュレーションサービス「DevPick」のフィードで、ページ送りの仕組みを作り直しました。DevPickは、ITmedia AI+・ITmedia NEWS・ASCII.jp・INTERNET Watchといった開発関連のニュースサイトをAIが横断してスコアリングし、その人の興味に合ったものだけを届けるWebアプリです。表示済みのIDを積み増していく方式をやめて、並び順を一度だけ固定してキャッシュする方式に変えたのですが、その過程で「並び順そのものが一意に決まっていない」という別の不具合にも気づきました。今回はその一連の経緯を書きます。

目次

表示済みIDを積み増す方式の限界

DevPickのログイン中のフィードは、記事に興味度スコアを付けて並べ替えて表示します(この並べ替えが何をしているかはDevPickの機能を紹介した記事で説明しています)。全記事をその場でスコアリングするわけにはいかないので、直近の記事から一定件数を取り出して、その中でスコアリングしています。

ここでページ送りをオフセット(何件目から何件目まで、という指定)だけで実装できないのが厄介なところでした。取り出す件数とスコアリングはリクエストのたびに走るので、その間にバックグラウンドの分析が終わったり、ユーザーが記事を評価したりすると、次のリクエストでは並びが変わってしまいます。並びが変わった状態でオフセットだけを進めると、記事が重複したり飛ばされたりするわけです。

そこで元の実装は、表示済みのIDを全部リクエストに載せて「これはもう見せたから除いて」と毎回渡す方式にしていました。並びが変わっても、既に見せたものは確実に除かれるので重複は起きません。その場しのぎに見えますが、当時の要件だけを見れば正しく動く作りではありました。

ただこの方式には副作用がありました。ページを進めるほど、除外リストが太っていってリクエストが重くなること。そして、5ページ目に直接飛びたくても、1ページ目から順に4回リクエストを送って除外リストを育てるしかないことです。

IT女子 アラ美
除外リストがどんどん育っていくの、想像するだけでちょっと嫌な予感がするんだけど。

ITアライグマ
まさにそこがボトルネックでした。ページ番号への直接ジャンプもできないので、地味に不便だったんです。

並びを固定する方式に切り替えた

除外リスト方式は、「並びは毎回変わるもの」という前提を受け入れた上で、変わっても困らないようにする作りでした。リクエストが太っていくのも、ジャンプできないのも、その前提から出てくる結果です。裏を返すと、前提を残したまま細かい改善を積んでも、この2つは消えません。

そこで前提のほうを変えることにしました。絞り込み条件が決まった時点で並び順を一度だけ計算し、その並びをキャッシュに保存します。以降のページ送りは、その保存済みの並びに対するオフセット指定だけで完結させます。

こうするとリクエストに載るのは、保存した並びを指す短いIDだけになります。ページを何ページ進めても重くなりませんし、任意のページ番号へ1回のリクエストで飛べるようになりました。

トレードオフは、並びがキャッシュの有効期間のあいだ固定されることです。その間にユーザーが記事を評価しても、順序には反映されません。ただ、元の除外リスト方式でも並びの再計算はリクエスト単位でしか起きず、表示中のページの並びが評価の直後に動くわけではありませんでした。「並びがすぐには追従しない」という性質自体は、方式を変える前から同じだったことになります。

有効期間は10分にしました。長くすればキャッシュは効きますが、古い並びを引きずる時間もそのまま伸びます。ページ送りを続けている一連の操作をまたげれば十分なので、短めに倒しています。

IT女子 アラ美
「並びをちょっとだけ諦める」って、言うのは簡単だけど実際どのくらいの時間にするか悩みそう。

ITアライグマ
そうなんです。長すぎると古い並びを引きずるので、10分という短めの値に倒しました。

作業中に見つかった、並び順そのものの漏れ

方式を作り直す過程で、ログインしていない状態のフィードを見直したときに、別の問題に気づきました。

未ログインのフィードは個人化のスコアリングが要らないので、DB側でそのままオフセット指定のページ送りをしています。ところがこの並び順のキーが、記事の公開日時だけになっていました。同じ値の行同士の順序はデータベースが保証してくれないので、ページの境目で記事が重複したり抜け落ちたりする状態だったのです。しかも公開日時は、入っていない記事もあるカラムでした。

問題なのは、これとまったく同じ不具合が、いいね一覧・興味なし一覧では既に直されていたことです。そちらには「重複しないように」という意図のコメント付きで一意なキーが足してありました。なのに、ログインしていない側の一覧だけがその修正から漏れていたのです。

原因はシンプルで、並び順をエンドポイントごとに手で書いていたからでした。一度どこかで気づいて直しても、同じ並び順を使っている別の入り口には自動的には伝わりませんし、次に一覧を追加するときに同じ書き方をしてしまう余地も残ります。なので今回は「未ログインの経路にも一意なキーを足す」で終わらせず、並び順の定義そのものを1箇所にまとめて、フィード系の全エンドポイントがそこを参照する形にしました。並び順を書く場所が1つしかなければ、次に一覧が増えても漏れようがありません。AIコーディング時代の開発フロー再設計で触れられているような、同じ種類の抜けを仕組みで止める発想が要るところだと感じました。

IT女子 アラ美
隣の一覧では直ってたのに、こっちだけ漏れてたって、あるあるすぎて笑えないやつだよね。

ITアライグマ
まさにそのパターンでした。個別に直すだけでは、次の一覧でまた繰り返していたと思います。

新しく決めなければならなくなったこと

ページ送りの状態をサーバー側のキャッシュに持たせるようにしたので、元の方式には無かった判断がいくつか増えました。

ひとつは、キャッシュが読めなかったときにどうするかです。保存も読み出しも失敗する可能性があるので、失敗したら元のやり方(毎回並びを計算し直す)に戻す、という方針にしました。ページ送りの効率だけが落ちて、フィードの中身自体は常に正しく返る形です。逆に「読めなかったから素通しする」ような作りにすると、キャッシュが落ちた瞬間に一覧の中身そのものが壊れます。

もうひとつは、自分でレビューしていて後から気づいたのですが、他人が保存した並びを読めてしまう問題でした。最初の実装では、保存していたのは並び順と件数だけで、それを誰が作ったものかを持たせていなかったのです。受け取った側も、そのIDが本当にリクエスト元のものかを確認していませんでした。

記事の中身そのものは呼び出し元の絞り込み条件で取り直すので漏れませんし、IDはランダムな値で有効期間も10分なので、当てにいくのも現実的ではありません。それでも、所有者を確認せずに読んでいること自体が権限チェックの漏れです。保存する内容に発行元を持たせて、読み出し時に照合するように直しました。

このとき、照合を「やってもやらなくてもいい」形にはしていません。読み出しの関数が発行元を引数として必ず要求するようにして、渡し忘れたらそもそも呼べないようにしています。不一致だった場合は有効期間切れと同じ経路、つまり並びを作り直して新しいIDを発行する流れに乗るので、呼び出し側に分岐は増えません。こうした後追いの気づきを拾うために、ClaudeCodeによる高速開発のための品質担保Tipsで書いたレビューの運用を続けています。

IT女子 アラ美
キャッシュが落ちたときの逃げ道を先に決めておくの、地味だけど大事だよね。

ITアライグマ
はい。補助的な仕組みは、落ちたときに少し不便になるだけで済む側に倒すようにしています。

まとめ

除外リストを積み増す方式は、「並びは毎回変わる」という前提を受け入れた作りだったので、リクエストが重くなる・ページジャンプできないという制約を最初から抱えていました。これは実装が粗かったというより、方式そのものが持っていた制約でした。前提そのものを変えて並びを一度固定する方式にしたことで、その2つは解消できましたが、代わりにキャッシュ障害時の逃げ道や、保存した並びを誰が読めるかという新しい判断が必要になりました。

作業の途中で見つけた「同じ並び順の修正が、隣の一覧には伝わっていなかった」という話も含めて、機能を作り直すときは、直した箇所だけでなく同じ性質を持つ他の入り口まで見て回る必要があると改めて感じた作業でした。

今回書いた仕組みは、DevPickで実際に動いています。開発関連ニュースの情報収集に興味がある方は、よかったら触ってみてください。

IT女子 アラ美
1つ直したつもりで、実は2つ目、3つ目が隠れてたってこと、結構あるよね。

ITアライグマ
そうですね。今回も、方式を変える作業をしていなければ気づかなかった漏れでした。

作者が開発したサービス「DevPick」

この記事をシェアする
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

ITアライグマのアバター ITアライグマ ITエンジニア / PM

都内で働くPM兼Webエンジニア(既婚・子持ち)です。
AIで作業時間を削って実務をラクにしつつ、市場価値を高めて「高年収・自由な働き方」を手に入れるキャリア戦略を発信しています。

目次