💡

Cloudflare Workers AI + Vectorizeを試す

に公開

概要

今回は、Cloudflare Workers AIとVectorizeの基本的な使い方を実際に手を動かしながら試してみたいと思います。Workers AIではLLMモデルを使った文章生成や多言語翻訳を、Vectorizeではベクトルデータベースの構築と検索を、最後にWorkers AIとVectorizeを合わせたサンプルを試してみます。

Workers AIで使えるモデルの一覧

https://developers.cloudflare.com/workers-ai/models/

Cloudflare Workers AI では、50 以上の OSS モデルを“サーバーレス GPU”で即時呼び出せるようにカタログ化しており、テキスト生成から画像生成、音声認識まで幅広いタスクを同じ API で扱えます。

開発環境

$ node -v
v22.7.0
$ yarn -v
1.22.22

シンプルな構成の Workers AI を試す

まずはWorkers AIがどんな感じか試してみたいと思います。

早速環境構築していきます。

$ mkdir cloudflare-workers-ai-example
$ cd cloudflare-workers-ai-example
$ npm create cloudflare@latest
# 以下の選択でプロジェクト作成しました
 Create an application with Cloudflare Step 1 of 3

 In which directory do you want to create your application?
 dir ./app

 What would you like to start with?
 category Hello World example

 Which template would you like to use?
 type Worker only

 Which language do you want to use?
 lang TypeScript

 Copying template files
 files copied to project directory

 Updating name in `package.json`
 updated `package.json`

 Installing dependencies
 installed via `npm install`

 Application created

 Configuring your application for Cloudflare Step 2 of 3

 Installing wrangler A command line tool for building Cloudflare Workers
 installed via `npm install wrangler --save-dev`

 Retrieving current workerd compatibility date
 compatibility date 2025-06-06

 Generating types for your application
 generated to `./worker-configuration.d.ts` via `npm run cf-typegen`

 You're in an existing git repository. Do you want to use git for version control?
│ no git

╰ Application configured

╭ Deploy with Cloudflare Step 3 of 3

├ Do you want to deploy your application?
│ no deploy via `npm run deploy`

╰ Done

プロジェクトが作成されたら wrangler.jsonc に以下を追加します。

{
  // ....
  "observability": {
    "enabled": true
  },
  // 👇追加
  "ai": {
    "binding": "AI"
  }
}

次に必要なパッケージをインストールしときます。

npm install @cloudflare/ai

worker-configuration.d.tsEnv に以下を追加するか、

interface Env extends Cloudflare.Env {
  AI: Ai; // 追加
}

以下コマンドでworker-configuration.d.ts を更新します。

npx wrangler types

src/index.ts を以下に変更します。

import { Ai } from '@cloudflare/ai';
export default {
  async fetch(request, env) {
    const ai = new Ai(env.AI);
    const input = { prompt: "What's the origin of the phrase 'Hello, World'" };
    const output = await ai.run('@cf/meta/llama-2-7b-chat-int8', input);
    return new Response(JSON.stringify(output));
  },
} satisfies ExportedHandler<Env>;

これで動かすだけですが、Workers AI はローカルだと動かせないので --remote をつけて手元でCloudflareの環境上で動かしてみたいと思います。

事前にログインしときます。

$ npx wrangler login
# ちゃんとログインできたか👇で確認
$ npx wrangler whoami

ログインできたら👇で動かしてみます。

npx wrangler dev --remote

起動してブラウザで http://localhost:8787/ にアクセスすると以下の様なレスポンスが返ってきます。

{
   "response":"What a great question!\n\nThe phrase \"Hello, World\" has a fascinating history! It originated in the early days of computer programming, specifically in the 1970s. The phrase was first used as a test message in the development of programming languages and operating systems.\n\nThe most commonly cited source of the phrase is Brian Kernighan, a computer scientist who worked at Bell Labs, a research and development organization. In 1973, Kernighan and his colleague, Dennis Ritchie, created the C programming language. They used \"Hello, World\" as a test program to ensure that their language was working correctly. The program printed the phrase \"Hello, World\" to the screen, and it has since become a standard phrase in programming culture.\n\nThe phrase was likely chosen because it was a simple, yet effective, way to test the basic functionality of a program. It's also a friendly and approachable greeting, which has contributed to its widespread adoption.\n\nOver time, the phrase has become a cultural phenomenon, symbolizing the beginning of a new program, a new project, or even a new era in technology. It's often used as a placeholder or a test message in various programming contexts, such as:\n\n1. Programming languages: Many programming languages, including C,",
   "usage":{
      "prompt_tokens":56,
      "completion_tokens":256,
      "total_tokens":312
   }
}

別のモデルも試してみる

先ほど試したのが llama-2-7b-chat-int8 というモデルで

Meta が公開した対話特化型 LLM Llama 2-Chat の 7 B(≒70 億)パラメータ版を、推論時に int8 量子化して軽量化したモデル
コンテキストウィンドウ 8 k トークンで比較的長い入力にも対応し、Transformer 系アーキテクチャを採用し、自然な対話や文章生成に強い

という特徴があるモデルでした。

今度は m2m100-1.2b というモデルを試してみたいと思います。特徴は以下になります。

Meta(旧 Facebook)が公開した 1.2 billion (約12 億)パラメータの Transformer エンコーダ-デコーダ型 LLM です。最大の特長は、100 言語 × 99 方向=9,900 通りの翻訳を 英語を介さずに 直接こなせる「Many-to-Many」設計で、低資源言語同士でも精度が高いことです

ということで英語→日本語の翻訳を試してみます。先ほどの src/index.ts を以下に書き換えます。

import { Ai } from '@cloudflare/ai';
export default {
  async fetch(request, env) {
    const ai = new Ai(env.AI);
    const output = await ai.run('@cf/meta/m2m100-1.2b', {
      text: "Workers AI allows you to run AI models in a serverless way, without having to worry about scaling, maintaining, or paying for unused infrastructure. You can invoke models running on GPUs on Cloudflare's network from your own code — from Workers, Pages, or anywhere via the Cloudflare API.",
      source_lang: 'english',
      target_lang: 'japanese',
    });
    return new Response(JSON.stringify(output));
  },
} satisfies ExportedHandler<Env>;

実行して http://localhost:8787/ にアクセスすると以下のレスポンスが返ってきました。

{
   "translated_text":"Workers AI を使用すると、Serverless で AI モデルを実行できますが、未使用のインフラストラクチャのスケーリング、メンテナンス、または支払いについて心配する必要はありません。Cloudflare のネットワーク上の GPU で動作するモデルは、Workers、Pages から、または Cloudflare API を通じてどこからでも、独自のコードから呼び出すことができます。",
   "usage":{
      "prompt_tokens":76,
      "completion_tokens":104,
      "total_tokens":180
   }
}

いい感じで翻訳できていそうです。

シンプルな構成の Cloudflare Vectorize を試す

👇こちらの記事を参考に進めていきたいと思います。

https://developers.cloudflare.com/vectorize/get-started/intro/

先ほどの cloudflare-workers-ai-example プロジェクトを使って進めていきます。

まずはCloudflare上にVectorizeインデックスを作成します。

$ npx wrangler vectorize create tutorial-index --dimensions=32 --metric=euclidean
🚧 Creating index: 'tutorial-index'
 Successfully created a new Vectorize index: 'tutorial-index'
📋 To start querying from a Worker, add the following binding configuration to your wrangler.json file:

{
  "vectorize": [
    {
      "binding": "VECTORIZE",
      "index_name": "tutorial-index"
    }
  ]
}

ベクトルの次元数(32)とmetricは参考記事に通りに設定してます。この2つのパラメータは作成時にしか決定できず変更できないので、要件に応じて検討する必要がありそうです。

※ metricは似ている情報かどうかを判定する際に使用され、metricには euclidean(ユークリッド距離), cosine(コサイン距離), dot product(ドット積) を指定できるようです。それぞれの説明は以下参照。

ユークリッド距離, コサイン距離, ドット積のLLM解説

まず要点だけまとめると

ユークリッド距離は「まっすぐものさしで測る距離」、コサイン距離は「ベクトルがつくる角度の違い」、ドット積は「同じ向きをどれだけ共有しているか」を数字にしたものです。どれも“‐5 メートル”のような変な値は出ず、0より大きいか、0に近いほど似ている/近いと判断できますが、測っているもの(長さ vs 向き)が違います。


距離と向きって何だろう?

  • *点や矢印(ベクトル)**を二次元の紙や三次元の空間に置くとき、「どれくらい離れているか」と「どの方向を向いているか」は別の情報になります。(datacamp.com)
  • ベクトルの“長さ”を測るのが距離系、角度を測るのがコサイン系、向き同士の重なり具合を測るのがドット積系だ、と覚えると混乱しません。(betterexplained.com, geeksforgeeks.org)

ユークリッド距離:ものさしで測る“まっすぐ距離”

イメージ 数学的ポイント
2点間に糸をピンと張って、その糸の長さを定規で測るイメージ。 ピタゴラスの定理で導かれ、直線距離=√((x₂−x₁)²+(y₂−y₁)²…)。
  • 「鳥が一直線に飛ぶときの移動距離」を思い浮かべると一発でわかります。(geeksforgeeks.org)
  • 次元が増えても“それぞれの座標差の2乗を足してルートを取る”という手順は同じです。(cuemath.com, byjus.com)
  • だから数が大きいほど遠く、小さいほど近いとストレートに読めるのが長所です。(youtube.com)

コサイン距離(コサイン類似度):ベクトルの“角度”を見る

イメージ 数学的ポイント
2本の矢印の挟む角度が小さいほど「似ている」。 cos θ = (𝐚・𝐛)/(‖𝐚‖‖𝐛‖)。類似度=cos θ、距離=1−cos θ がよく使われる。
  • 長さをぜんぶ1にそろえてから角度だけ比べている、と思うとイメージしやすいです。(medium.com)
  • 角度が0°なら cos θ=1(完全に同じ向き)、90°なら0(直交=まったく方向が違う)になるので値は −1〜1。0.8 など大きいほど似ています。(geeksforgeeks.org, datastax.com)
  • 文章比較など「文の長さが違っても話題が同じか」を調べたいときによく使われます。(machinelearningplus.com)
  • 動画で見ると「角度が開くほどバーが下がる」アニメがわかりやすいです。(youtube.com)

ドット積(内積):向きの重なり具合を測る

イメージ 数学的ポイント
片方の矢印を“影”のようにもう片方に落とし、その影の長さをかけ合わせる感じ。 𝐚・𝐛 =
  • 正になると「だいたい同じ方向」、0なら直交、負なら逆方向と簡単に判定できます。(khanacademy.org, mathsisfun.com)
  • ベクトル同士を「掛け算」してもベクトルではなくただの数が返ってくるので“スカラー積”とも呼ばれます。(betterexplained.com)
  • 力と距離で仕事量(エネルギー)を計算するときなど、物理でも登場します。(youtube.com)

どう使い分ける?

シーン おすすめ指標 理由
地図で2地点の“実際の距離”を測りたい ユークリッド距離 直線でどれだけ離れているかが知りたいだけだから
文章や画像の「内容が似ているか」を比べたい コサイン距離 / 類似度 文字数や画素数(=長さ)が違っても向き(特徴の比率)が似ていれば OK
力の向きと移動方向の関係など、向きの重なり度合いを数値化したい ドット積 正負や大きさで一石二鳥に判定できる
  • 長さそのものが重要→ユークリッド方向だけ重要→コサイン方向+強さをまとめて1つの数字で済ませたい→ドット積と覚えると便利です。(geeksforgeeks.org, datastax.com, khanacademy.org)

まとめ

指標 何を測る? 値域 0 に近いと?
ユークリッド距離 点どうしの直線距離 0 ~ ∞ とても近い
コサイン距離 (1−cos θ) ベクトル間の角度差 0 ~ 2 向きがほぼ同じ
ドット積 向きの重なり+大きさ −∞ ~ ∞ 方向直交(=0)

これで「長さ」「角度」「向きの重なり」という3つの切り口の違いがつかめたと思います。どんなデータでも**“何を比べたいか”**を先に決めてから、ぴったりの指標を選ぶようにしましょうね。

次にターミナルの実行結果にもあった通り wrangler.jsonc に以下を追加します。

{
  "vectorize": [
    {
      "binding": "VECTORIZE",
      "index_name": "tutorial-index"
    }
  ]
}

worker-configuration.d.ts も合わせて更新します。

declare namespace Cloudflare {
  interface Env {
    VECTORIZE: VectorizeIndex; // 手動で追加、または npx wrangler types 実施
    AI: Ai;
  }
}

次は実際にベクトルをインデックスに登録してみます。先ほどの src/index.ts を以下に修正します。

import { Ai } from '@cloudflare/ai';

const sampleVectors: Array<VectorizeVector> = [
  {
    id: '1',
    values: [
      0.12, 0.45, 0.67, 0.89, 0.23, 0.56, 0.34, 0.78, 0.12, 0.9, 0.24, 0.67, 0.89, 0.35, 0.48, 0.7, 0.22, 0.58, 0.74, 0.33, 0.88, 0.66,
      0.45, 0.27, 0.81, 0.54, 0.39, 0.76, 0.41, 0.29, 0.83, 0.55,
    ],
    metadata: { url: '/products/sku/13913913' },
  },
  {
    id: '2',
    values: [
      0.14, 0.23, 0.36, 0.51, 0.62, 0.47, 0.59, 0.74, 0.33, 0.89, 0.41, 0.53, 0.68, 0.29, 0.77, 0.45, 0.24, 0.66, 0.71, 0.34, 0.86, 0.57,
      0.62, 0.48, 0.78, 0.52, 0.37, 0.61, 0.69, 0.28, 0.8, 0.53,
    ],
    metadata: { url: '/products/sku/10148191' },
  },
  {
    id: '3',
    values: [
      0.21, 0.33, 0.55, 0.67, 0.8, 0.22, 0.47, 0.63, 0.31, 0.74, 0.35, 0.53, 0.68, 0.45, 0.55, 0.7, 0.28, 0.64, 0.71, 0.3, 0.77, 0.6, 0.43,
      0.39, 0.85, 0.55, 0.31, 0.69, 0.52, 0.29, 0.72, 0.48,
    ],
    metadata: { url: '/products/sku/97913813' },
  },
  {
    id: '4',
    values: [
      0.17, 0.29, 0.42, 0.57, 0.64, 0.38, 0.51, 0.72, 0.22, 0.85, 0.39, 0.66, 0.74, 0.32, 0.53, 0.48, 0.21, 0.69, 0.77, 0.34, 0.8, 0.55,
      0.41, 0.29, 0.7, 0.62, 0.35, 0.68, 0.53, 0.3, 0.79, 0.49,
    ],
    metadata: { url: '/products/sku/418313' },
  },
  {
    id: '5',
    values: [
      0.11, 0.46, 0.68, 0.82, 0.27, 0.57, 0.39, 0.75, 0.16, 0.92, 0.28, 0.61, 0.85, 0.4, 0.49, 0.67, 0.19, 0.58, 0.76, 0.37, 0.83, 0.64,
      0.53, 0.3, 0.77, 0.54, 0.43, 0.71, 0.36, 0.26, 0.8, 0.53,
    ],
    metadata: { url: '/products/sku/55519183' },
  },
];

export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    // 先ほどのWorkers AIサンプル
    if (path.startsWith('/ai')) {
      const ai = new Ai(env.AI);
      const output = await ai.run('@cf/meta/m2m100-1.2b', {
        text: "Workers AI allows you to run AI models in a serverless way, without having to worry about scaling, maintaining, or paying for unused infrastructure. You can invoke models running on GPUs on Cloudflare's network from your own code — from Workers, Pages, or anywhere via the Cloudflare API.",
        source_lang: 'english',
        target_lang: 'japanese',
      });
      return new Response(JSON.stringify(output));
    }

    if (path.startsWith('/insert')) {
      const inserted = await env.VECTORIZE.insert(sampleVectors);
      return Response.json(inserted);
    }
    return new Response('nothing to do... yet', { status: 404 });
  },
} satisfies ExportedHandler<Env>;

/insert にリクエストがあると sampleVectors を登録するような実装になっています。早速登録してみたいと思います。

$ npx wrangler dev --remote
# 別ターミナルで
$ curl http://localhost:8787/insert
{
   "mutationId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}

次に実際にクエリして確認してみます。src/index.tsに以下を追加します。

export default {
  async fetch(request, env) {
    const path = new URL(request.url).pathname;
    // ....
    // 👇追加
    if (path.startsWith('/query')) {
      const queryVector: Array<number> = [
        0.13, 0.25, 0.44, 0.53, 0.62, 0.41, 0.59, 0.68, 0.29, 0.82, 0.37, 0.5, 0.74, 0.46, 0.57, 0.64, 0.28, 0.61, 0.73, 0.35, 0.78, 0.58,
        0.42, 0.32, 0.77, 0.65, 0.49, 0.54, 0.31, 0.29, 0.71, 0.57,
      ];
      const matches = await env.VECTORIZE.query(queryVector, {
        topK: 3,
        returnValues: true,
        returnMetadata: 'all',
      });
      return Response.json({ matches });
    }
    return new Response('nothing to do... yet', { status: 404 });
  },
} satisfies ExportedHandler<Env>;

ちゃんとクエリできるか試してみます。

$ curl http://localhost:8787/query
{
   "matches":{
      "count":3,
      "matches":[
         {
            "id":"4",
            "score":0.46348256,
            "values":[
              0.17, 0.29, 0.42, 0.57, 0.64, 0.38, 0.51, 0.72, 0.22, 0.85, 0.39,
              0.66, 0.74, 0.32, 0.53, 0.48, 0.21, 0.69, 0.77, 0.34, 0.8, 0.55, 0.41,
              0.29, 0.7, 0.62, 0.35, 0.68, 0.53, 0.3, 0.79, 0.49
            ],
            "metadata":{
               "url":"/products/sku/418313"
            }
         },
         {
            "id":"3",
            "score":0.52920616,
            "values":[
              0.21, 0.33, 0.55, 0.67, 0.8, 0.22, 0.47, 0.63, 0.31, 0.74, 0.35, 0.53,
              0.68, 0.45, 0.55, 0.7, 0.28, 0.64, 0.71, 0.3, 0.77, 0.6, 0.43, 0.39,
              0.85, 0.55, 0.31, 0.69, 0.52, 0.29, 0.72, 0.48
            ],
            "metadata":{
               "url":"/products/sku/97913813"
            }
         },
         {
            "id":"2",
            "score":0.6337869,
            "values":[
        "values": [
              0.14, 0.23, 0.36, 0.51, 0.62, 0.47, 0.59, 0.74, 0.33, 0.89, 0.41,
              0.53, 0.68, 0.29, 0.77, 0.45, 0.24, 0.66, 0.71, 0.34, 0.86, 0.57,
              0.62, 0.48, 0.78, 0.52, 0.37, 0.61, 0.69, 0.28, 0.8, 0.53
            ],
            "metadata":{
               "url":"/products/sku/10148191"
            }
         }
      ]
   }
}

ちゃんとクエリできてそうです ✨

Workers AI + Vectorizeを試す

最後にWorkers AI + VectorizeでRAGもどきを作ってみたいと思います。渡されたテキストをベクトル化するモデルには以下を使います、

https://developers.cloudflare.com/workers-ai/models/bge-base-en-v1.5/

任意のテキストを768次元ベクトルに変換するBAAI一般埋め込みモデル
※ BAAI General Embedding(BGE)は、北京智源人工知能研究院 (BAAI) が公開した汎用テキスト埋め込みモデル群

768次元ベクトル という事なのでVectorizeインデックスを再度作り直したいと思います。

# 必要あれば先ほどのインデックスを削除
$ npx wrangler vectorize list
┌────────────────┬────────────┬───────────┬─────────────┬────────────────────────────┬────────────────────────────┐
 name dimensions metric description created modified
├────────────────┼────────────┼───────────┼─────────────┼────────────────────────────┼────────────────────────────┤
 tutorial-index 32 euclidean .......................... ..........................
└────────────────┴────────────┴───────────┴─────────────┴────────────────────────────┴────────────────────────────┘
$ npx wrangler vectorize delete tutorial-index
# 768次元ベクトルでmetricがcosineのものを作成
$ npx wrangler vectorize create embeddings-index --dimensions=768 --metric=cosine

wrangler.jsonc を更新しときます。

{
  "vectorize": [
    {
      "binding": "VECTORIZE",
      "index_name": "embeddings-index"
    }
  ]
}

src/index.ts を以下に変更します。

interface EmbeddingResponse {
  shape: number[];
  data: number[][];
}

export default {
  async fetch(request, env) {
    const url = new URL(request.url);
    const path = url.pathname;
    const userQuery = url.searchParams.get('q') ?? '';

    if (path.startsWith('/insert')) {
      const stories = [
        'Orange cloud drifts above the silent sea',
        'A llama hums softly at snowy dawn',
        'Mars robot nurtures tomatoes beneath pink sky',
      ];
      const modelResp: EmbeddingResponse = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
        text: stories,
      });
      const vectors: VectorizeVector[] = [];
      let id = 1;
      modelResp.data.forEach((vector) => {
        vectors.push({ id: `${id}`, values: vector });
        id++;
      });

      let inserted = await env.VECTORIZE.upsert(vectors);
      return Response.json(inserted);
    }
    if (path.startsWith('/query')) {
      const queryVector: EmbeddingResponse = await env.AI.run('@cf/baai/bge-base-en-v1.5', {
        text: [userQuery],
      });

      const matches = await env.VECTORIZE.query(queryVector.data[0], {
        topK: 1, // 類似検索で返す件数
      });
      return Response.json({ matches });
    }
    return new Response('nothing to do... yet', { status: 404 });
  },
} satisfies ExportedHandler<Env>;

通常はSQL databaseなどから引っ張ってきたデータをVectorizeインデックスに登録すると思いますが、今回はLLMに考えてもらった以下のテキストを上からid=1 の順で登録しています。

id テキスト
1 Orange cloud drifts above the silent sea
2 A llama hums softly at snowy dawn
3 Mars robot nurtures tomatoes beneath pink sky

早速試してみたいと思います。

$ npx wrangler dev --remote
# 別ターミナルで
$ curl http://localhost:8787/insert
{
   "mutationId":"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
}
# “robot vegetables” のワードでクエリ
$ curl http://localhost:8787/query?q=robot+vegetables
{
   "matches":{
      "count":1,
      "matches":[
         {
            "id":"3",
            "score":0.8041519
         }
      ]
   }
}

スコアが 1 に近いほどマッチしているので 0.8.. なのでいい感じでクエリできてそうです。

他のワードでも試してみます。

# "winter" だけでクエリ
$ curl http://localhost:8787/query?q=winter
{
   "matches":{
      "count":1,
      "matches":[
         {
            "id":"2",
            "score":0.6488873
         }
      ]
   }
}
# “winter animal” のワードでクエリ
$ curl http://localhost:8787/query?q=winter+animal
{
   "matches":{
      "count":1,
      "matches":[
         {
            "id":"2",
            "score":0.7506682
         }
      ]
   }
}

想像通りのスコアになってます。

Workers AI + Vectorizeで簡単に試せて雰囲気が把握できたので良かったです!次回は別のモデルや日本語で試したり、もっと踏み込んで試せたらと思います。

バッドノウハウ

Workers AI を動かした際に以下の様なエラーが出る

 [ERROR] Could not resolve "base64-js"

解決策としては単純に base64-js を追加してあげればOKです。

npm i base64-js

参考URL

https://zenn.dev/tmsic/articles/82dc27ebe702d2

https://zenn.dev/laiso/articles/7a21b5bf14f10c

https://zenn.dev/kameoncloud/articles/fcdf5b7ee3e3a3

https://zenn.dev/kameoncloud/articles/707b3b623bdb87

Discussion