👌

Momento Cache + D1 を使って簡易絞り込み機能を作る

2023/12/10に公開

Momento Cache + D1 を使って簡易絞り込み機能を作る

やりたいこと

前回記事

この課題

2023-12-03.png

  • こちらは結果、DictionaryGetFields でまとめて取ることで解決しました。
参考サイト

https://zenn.dev/momentobigfun/articles/7bd04650162aed

とはいえ

momento cache はキー指定か、全部取得しかできない。絞り込みはListやスコアなどを使って、値を使った内容のフィルタはできる。

Momentoは

  • キャッシュなので一発引きするのは適している
  • まとめて取る方法はあるが、絞り込みはキーを指定する必要があるので、キーを知らない場合は利用できない

なんとか絞り込み機能をつけたい

どうやって?

Cloudflare には D1がある。D1 はsqlite3 のため、SQLを使って絞り込みができる。

システム要件

  • 前回のシステムに下記を追加する。今回の内容を入れていますので、こちらを確認してください。

バックエンド

  • Cloudflare D1 キーワード登録時に、indexを作成する
  • Worker でキーワードを受け取る
  • 下記のクエリを投げるselect asin from books where Title like '%{input}%'
  • キーのリストが取得できるので、それを使って、DictionaryGetFields でまとめて取得する

作る

Cloudflare D1構築

今回はサクッと Drizzle ORM を使って構築します。

Drizzle のインストールはこちらで

キャッシュテーブル

src/db/schema/books.ts
export const books = sqliteTable(
	"books",
	{
		asin: text("asin").primaryKey(),
		Title: text("Title").notNull(),
	},
	(books) => ({
		asinIdx: uniqueIndex("asinIdx").on(books.asin),
		TitleIdx: uniqueIndex("TitleIdx").on(books.Title),
	}),
);

wrangler.toml

D1 設定を追加する

config/wrangler.dev.toml
[[ d1_databases ]]
binding = "DB" # i.e. available in your Worker on env.DB
database_name = "asin"
database_id = ""
migrations_dir = "drizzle"

migrateファイル作成&migrate

npx drizzle-kit generate:sqlite --schema=src/db/schema/books.ts --out=config/drizzle
npx wrangler d1 migrations apply asin -c config/wrangler.dev.toml

前回のスクリプト修正

-e 指定をしていると D1 Bindingsがうまくできない

ここの -e dev を外します。これでも.dev.varsは読み込まれます。

wrangler dev -c config/wrangler.dev.toml -e dev

プログラム概要

キャッシュインデックスの作成(/momento/index/create)

すでにキャッシュされているキャッシュからインデックスを作成します。

  1. Dictionary から fetch して、レコードを取得
  2. ASINをキー、Titleを値として、インデックスを作成して、D1に保存します。
src/momento/index_builder.ts
public static async build(cache: Cache, cacheDB: D1Database) {
    const values = await cache.fetch();
    const keys = [];
    for (const value of values) {
        keys.push({ asin: value[0], Title: JSON.parse(value[1]).Title });
    }
    return drizzle(cacheDB).insert(books).values(keys).onConflictDoNothing();
}

キャッシュインデックスのリストアップ(/momento/index)

テスト用にindexを返却します。

index例
[
{
    "asin":"B0C8YJRGW8",
    "Title":"OpenAI GPT-4/ChatGPT/LangChain 人工知能プログラミング実践入門"
},
{
    "asin":"B0CBR9GYF6",
    "Title":"GitLabに学ぶ 世界最先端のリモート組織のつくりかた ドキュメントの活用でオフィスなしでも最大の成果を出すグローバル企業のしくみ"
},
{
    "asin":"B0CJRDFJZB",
    "Title":"ChatGPT/LangChainによるチャットシステム構築[実践]入門"
}]

キャッシュに登録されているTitleからの検索(/momento/search)

src/momento/index_builder.ts
public static async search(
    cache: Cache,
    keyword: string,
    cacheDB: D1Database,
) {
    const keys = await drizzle(cacheDB)
        .select({ asin: books.asin })
        .from(books)
        .where(like(books.Title, `%${keyword}%`));
    return cache.gets(keys.map((key) => key.asin));
}
使い方
  • Request
curl -X GET "https://paapi5_worker.future-techno-developers.workers.dev/momento/search?keyword=ChatGPT"

感想

  1. D1とMomento Cache の組み合わせは案外相性が良い
    • D1はTransactionがなかったり、レコードが増えると重くなる傾向があるので、indexレベルの単機能で使用するのが良さそう
    • Momento Cache は検索フィルタ機能はないので、外側で戦う必要はある。Cloudflare D1と組み合わせると良さそう
  2. D1 と Momento Cache のレコードの同期が課題ではあるけど、なければDictionary で取れないだけだと思うので、それほど問題ではなさそう

最後に

今回の機能とは少しズレますが、Momentoはシンプルな機能が多いので、完全なRedis互換だと思っていると案外できないことが多いので、 外側で色々作る必要が出てきます。
クエリキャッシュなど1:1のキャッシュの場合は良いですが、検索したり色々制御を入れたい場合は制御を作りましょう。

GitHubで編集を提案

Discussion