Cloudflare、Remix、OpenAI Embeddingsでニッチな投稿を優遇するSNS風サービスを作った
はじめに
この記事では、以下の技術スタックを使って、ニッチな投稿を優遇するSNS風サービスを作成した話をします。
インフラ
メインコード
テスト
モニタリング、監視
最近Cloudflare D1がGAしたり、Hyperdriveが更新されたりと、Cloudflareの進化が著しいですね!
似たような技術スタックのサービスを作る際の参考になればと思います。
コンセプト
広告でマネタイズしているSNSサービスは、多数派のユーザーに刺さる内容がバズりやすくなるよう設計されていると思います。
さらに最近では、それをハックして毒にも薬にもならないような投稿をして、インプレッションを稼ぐインプレゾンビ的なユーザーも発生しています。
逆に言えば、バズりづらい尖ったニッチなコンテンツは既存のSNSサービスには向いていないと言えます。
ターゲティングを強化してある程度解決していますが、基本的には統計を元にしたアルゴリズムでユーザーの興味を予測しているため、やはり尖りすぎたコンテンツには向いていないように感じます。
そこで、むしろニッチなコンテンツがインプレッションを稼げる、そんなSNSサービスを作りたいと思いました。
機能
主な機能は以下の通りです。
- タグを付けて短文と画像を投稿
- AIがタグのニッチ度をスコアリング
- 投稿一覧ではニッチスコアが高い順に表示
- 他者の投稿に対してgood, badを付けられ、それによりニッチスコアを調整
これにより、まずはニッチなタグを付けること、そしてそれに沿った投稿が行われることを期待しています。
AIでスコアリングするのはコンセプトに反しているのでは?と思われるかもしれませんが、ユーザーによるgood, badによるスコアリングとの掛け合わせを調整することでバランスを取りたいと思っています。
技術選定
前提として、今回のサービスはマスに向けたマネタイズは難しいため、維持費を極端に抑えることで採算を取る方針です。
ここはSRE的な経験値を活かして、要件とコストのバランスを取って技術選定を行いました。
背景と選定理由
開発費用を下げる意味合いで開発者は基本的に私だけということで、バックエンドとフロントエンドを別個に作りたくなかったので、フルスタックなフレームワークを探しました。
最初は経験のあるNext.jsを使おうかと思いましたが、Edge Runtimeで動かす前提で作られていないこともありハマることが多かったです。
コスパの良いCloudflare Workersを使いたいと思っていたのもあり、そのようなタイミングでRemixを触ったところ、かなり書き味が良かったので採用に至りました。
それに合わせてDB周りはD1とDrizzleを、CSS周りはTailwindCSSを使いました。
Embeddingについては信頼性と価格のバランスが良いOpenAIのものを無難に使いました。Cloudflare AI Gatewayはまだベータですが、キャッシュやセキュリティの面で便利なので挟むこととしています。
モニタリングにも基本無料のGrafana CloudとLokiを使いました。自作の簡易ロギングライブラリもあるので良ければ使ってください。
技術選定まとめ
つまり、維持費を極端に抑える構成を取っています。これにより維持費は現状Embeddingsの料金のみで、かなりスケールするまではほぼ無いようなものだと思います。
発生した課題と解決策
ログイン処理
Googleでのログインを実装しているのですが、Remixの認証ライブラリであるRemix AuthがそのままだとEdge Runtimeで上手く動きませんでした。
この辺りの記事を参考に実装しました。
Drizzleで複雑なSQLを書く
Cloudflare Workersは実行時間の制約が厳しいため、なるべくSQL上でソート等の処理をしたかったです。
DrizzleはORMなので、複雑なSQLを書くのは難しいかと思われるかもしれませんが、SQLを直接書くこともできます。
これでサブクエリなどを直接書くことでなんとかできました。
Drizzleを初期化するとき
drizzle(db, { logger: true });
とすると実行時のSQLが出力されるので、それを見ながらデバッグすると良いと思います。
Cloudflare D1でアトミックな処理をする
Cloudflare D1はトランザクションに対応していないため、アトミックな処理をするのが難しいです。
ただし、複数のSQLをまとめて実行することはできるので、それを使ってなんとかしました。このあたりもDrizzleならシュッと書くことができます。
await db.batch([
db.insert(posts).values(newPost1),
db.insert(posts).values(newPost2)
]);
画面上で非同期読み込みをする
RemixはSSRしかないので、画面上での非同期更新を書くのは少し面倒でした。
Suspenseで囲み、lorderでdeferで返却することで実現できました。
雰囲気としてはこのような感じです。
import { defer, LoaderFunctionArgs } from "@remix-run/cloudflare";
import { Suspense } from "react";
import { Await } from "react-router";
export async function loader({ request, context }: LoaderFunctionArgs) {
const postsPromise = getPosts();
return defer({
posts: postsPromise,
});
}
export default function Post() {
return <Suspense fallback={<div>Loading...</div>}>
<Await
promise={postsPromise}
then={(posts) => {
return <PostList posts={posts} />;
}}
/>
</Suspense>
}
最近のReactの知識が無かったので結構苦労しました……
開発環境のアクセス制限
Cloudflare Pagesではブランチごとに環境を自動的に立ててくれますが、何もしなければそこにもユーザーはアクセスできてしまいます。
これを防ぐために、Cloudflare Accessを使って開発環境にアクセス制限をかけました。
*.${ドメイン}.pages.dev
に対してアクセス制限をかけています。
まとめ
CloudflareとRemixを使って、ニッチな投稿を優遇するSNS風サービスを作成した話について、具体的なトラブルシューティングや技術選定の理由について書きました。
Cloudflareのサービスはどんどん進化しているので、これからも注目していきたいですね!
ぜひNiche Loveにも投稿をお願いします!
質問などありましたらコメントもいただけると嬉しいです!
宣伝
2024/04/21(日)14:00 〜 18:00
生成AIなんでも展示会
という生成AIプロダクトを展示、閲覧できるイベントをやります!
展示枠は締め切っていますが、閲覧に関してはまだ参加登録できますのでよろしくお願いします!
(現状お金など動いていない完全ボランティア企画ですということは明記しておきます)
Discussion