Cloudflare Workersで類似度ベクトル検索APIを構築する
ChatGPT Retrieval PluginをJavaScriptで実装するついでにCloudflare Workersで動作するようにします。
Retrieval PluginはベクトルDBからのドキュメントの出し入れがほぼ行なっていることであり、検索APIさえ作れればあとは何とかなるなと思い先に実装しました。
Cloudflare Workersで——とは言ってもCloudflare Workersの環境はブラウザ+α程度の機能しか持たないため外部のサービスのいろいろなAPIを呼び出すことになります。API繋ぎ込み職人の朝は早い。
先ずベクトルDB側に検索データを入れておく必要があります。この記事ではベクトルDBにPineconeというサービスを使っています。ChatGPT Retriever PluginsをLangChainでデバッグするで実装したプラグインのバックエンドになります。
Pineconeを使わない場合LangchainJSドキュメントの「Vector Stores: Integrations | 🦜️🔗 Langchain」でリストされているような製品は選択肢になるはずです。
Cloudflare Workersの環境で動かしやすいものをという理由でサーバーレスなPineconeにしました。
> wrangler init
> npm init
> npm i openai @haverstack/axios-fetch-adapter @pinecone-database/pinecone@0.0.10
Workerの実装は以下のようになります。
- リクエストパラメータ
q
をEmbeddins APIで変換 - Pineconeのindex検索に渡す
name = "XXX"
main = "src/index.ts"
compatibility_date = "2023-04-30"
[vars]
PINECONE_INDEX = "XXX"
import fetchAdapter from "@haverstack/axios-fetch-adapter";
import { PineconeClient } from "@pinecone-database/pinecone";
import { Configuration, OpenAIApi } from "openai";
export interface Env {
OPENAI_API_KEY: '',
PINECONE_INDEX: '',
PINECONE_API_KEY: '',
PINECONE_ENVIRONMENT: '',
}
export default {
async fetch(
request: Request,
env: Env,
ctx: ExecutionContext
): Promise<Response> {
const requestUrl = new URL(request.url);
const params = requestUrl.searchParams;
const query = params.get('q') || '';
const topK = Number(params.get('top_k')) || 10;
if (query.length === 0) {
return new Response(JSON.stringify({ error: 'No query provided' }), {
headers: { "content-type": "application/json" },
});
}
const configuration = new Configuration({
apiKey: env.OPENAI_API_KEY,
baseOptions: {
adapter: fetchAdapter,
}
});
const openai = new OpenAIApi(configuration);
try {
const embeddingResponse = await openai.createEmbedding({
'model': 'text-embedding-ada-002',
'input': query,
});
const [{ embedding }] = embeddingResponse.data.data
console.log('created embeddings: '+ embedding.length);
const pinecone = new PineconeClient();
await pinecone.init({
environment: env.PINECONE_ENVIRONMENT,
apiKey: env.PINECONE_API_KEY,
});
const index = pinecone.Index(env.PINECONE_INDEX);
const queryResponse = await index.query({
queryRequest: {
topK,
includeMetadata: true,
vector: embedding,
},
});
return new Response(JSON.stringify(queryResponse), {
headers: { "content-type": "application/json" },
});
} catch (error) {
console.error(error);
return new Response(JSON.stringify(error));
}
},
};
Tips(1): openaiクライアントのリクエストadapterをfetchベースに置き換える
Node.js版のopenaiモジュールはnet/httpライブラリが呼び出せないのでそのまま動きません。
OpenAI APIクライアント部分をfetchで自作するか@vespaiach/axios-fetch-adapterのように同じ問題への対策として公開されているライブラリを使用します。
ただaxios本体にfetchアダプタが入るとそれを設定するだけに済みそうです。ロードマップ不明ですがプルリクエストはあります。
Tips(2): 古いバージョンのPineconeクライアントを使う
最新版のPineconeクライアントはNode.jsのprocess
変数に依存しているのでCloudflare Workersでは動作しません。
この問題もミニマムなAPIクライアントを自作するのが本来はよいのですが、コミュニティではprocess
が入る前のバージョンを使うワークアラウンドが知られているみたいです。
Workerの実行
サーバーの起動の前にenvに羅列したsecretsの設定が必要です(wranglerは開発時もCloudflareにアクセスしているため)。
PINECONE_INDEXは前述のwrangler.tomlで指定しました。
> npx wrangler secret put OPENAI_API_KEY
> npx wrangler secret put PINECONE_API_KEY
> npx wrangler secret put PINECONE_ENVIRONMENT
> wrangler dev
Pineconeの検索結果がそのまま返ってきていることを確認できました。
❯ curl -s "http://localhost:8787/?q=OpenAIを活用したWebサービスについて教えてください" | jq '.matches[0:3]'
[
{
"id": "https://qiita.com/mikito/items/5de238bb497fb18ab2db_18",
"score": 0.877836883,
"values": [],
"metadata": {
"created_at": 1680660000,
"document_id": "https://qiita.com/mikito/items/5de238bb497fb18ab2db",
"source": "qiita",
"text": "的達成です。 ただし、このようなアプリをサービスとして提供する場合は、技術的には以下の課題があるように感じました(権利と収益関係除く)。 1つ目は、OpenAIのAPI Keyをクライアン
に埋め込むわけにはいかないので、アプリから直接APIと通信していたところを、自前のサーバー経由で行う必要があるところです。さらに今回のように順次処理を行う場合はサーバーからPushできるような構
にしないといけなそうな気がします。 2つ目は",
"title": "おはなし無限読み上げアプリ「ずんだテラー」の開発ポイント解説 - Qiita",
"url": "https://qiita.com/mikito/items/5de238bb497fb18ab2db"
}
},
{
"id": "https://zenn.dev/k_kind/articles/chatapi-stream_1",
"score": 0.87604332,
"values": [],
"metadata": {
"created_at": 1681272600,
"document_id": "https://zenn.dev/k_kind/articles/chatapi-stream",
"source": "zenn",
"text": "OpenAIの APIを叩く方法が一番シンプルにできそうです。 https://zenn.dev/himanushi/articles/99579cf407c30b 注意点として、APIキーがユーザーに知られるため、ユースケースが限られ
うです。 方法2. Node.jsのサーバーを介してOpenAI APIを叩く 今回は、以下のように中間にNode.jsのサーバーを挟む例を示します。 Step1. Node.jsサーバーの実装 ※注意点として、サーバーは text/event-stream 形式のレスポンスを返す必要があり、 Next.js API Routes + VercelやAWS Amplifyのような、サーバーレス環�",
"title": "OpenAIのChat APIの返答をストリーミングする(Node.js)",
"url": "https://zenn.dev/k_kind/articles/chatapi-stream"
}
},
{
"id": "https://dev.classmethod.jp/articles/888c355f2c88e117d172ec1bd3d28a435ee438766630638e3e9f7887aef8f5ee/_12",
"score": 0.875625312,
"values": [],
"metadata": {
"created_at": 1681808880,
"document_id": "https://dev.classmethod.jp/articles/888c355f2c88e117d172ec1bd3d28a435ee438766630638e3e9f7887aef8f5ee/",
"source": "hatena",
"text": "で、履歴の記録・学習への利用をOffにする機能が実装されました! 江口佳記 2023.04.26 OpenAIのブランドガイドラインが公開されたので読んでみた 江口佳記 2023.04.26 ChatGPT に
文メール書くのを手伝ってもらった Ito ma 2023.04.25 独自AIチャットを簡単に自サイトに設置できる「DocsBot AI」を試してみた M.Shimizu 2023.04.25 クラスメソッド株式会社 主なカテゴリ AWS AWS特集 Amazon EC2",
"title": "OpenAI APIのFine-tuningを試してみる | DevelopersIO",
"url": "https://dev.classmethod.jp/articles/888c355f2c88e117d172ec1bd3d28a435ee438766630638e3e9f7887aef8f5ee/"
}
}
]
デプロイしてworkers.dev上でも動作していればok。
npx wrangler publish
Pinecone以外の選択肢
pgvectorとEdge Functionを使った例はSupabaseのブログで公開されています。
僕もSupabaseとCloudflare Workersの組合せで作っても良かったのですが、記事の想定読者はフロントエンドエンジニアなのでベクトルDBサービスの部分を外部化しやすいPineconeを選びました。
またTraining ChatGPT with Custom Libraries Using ExtensionsではRubyとRedisのRediSearch拡張による類似度ベクトル検索の構築が紹介されています。
Cloudflare Workers以外の選択肢
Cloudflare Workersではなく通常のNode.jsを動作環境にする場合はベクターDBの選択肢が増えます。
Node.jsでnoteアカウントの記事を参照してGPTに質問するではhnswlibを使い、ローカルにインデックスファイルを書き出す設計にしました。
Discussion