💭

Cloudflare Workersで類似度ベクトル検索APIを構築する

2023/04/30に公開

ChatGPT Retrieval PluginをJavaScriptで実装するついでにCloudflare Workersで動作するようにします。

Retrieval PluginはベクトルDBからのドキュメントの出し入れがほぼ行なっていることであり、検索APIさえ作れればあとは何とかなるなと思い先に実装しました。

Cloudflare Workersで——とは言ってもCloudflare Workersの環境はブラウザ+α程度の機能しか持たないため外部のサービスのいろいろなAPIを呼び出すことになります。API繋ぎ込み職人の朝は早い。

先ずベクトルDB側に検索データを入れておく必要があります。この記事ではベクトルDBにPineconeというサービスを使っています。ChatGPT Retriever PluginsをLangChainでデバッグするで実装したプラグインのバックエンドになります。

https://zenn.dev/laiso/articles/39174470be0608

Pineconeを使わない場合LangchainJSドキュメントの「Vector Stores: Integrations | 🦜️🔗 Langchain」でリストされているような製品は選択肢になるはずです。

https://js.langchain.com/docs/modules/indexes/vector_stores/integrations/

Cloudflare Workersの環境で動かしやすいものをという理由でサーバーレスなPineconeにしました。

> wrangler init
> npm init
> npm i openai @haverstack/axios-fetch-adapter @pinecone-database/pinecone@0.0.10

Workerの実装は以下のようになります。

  1. リクエストパラメータqをEmbeddins APIで変換
  2. Pineconeのindex検索に渡す
wrangler.toml
name = "XXX"
main = "src/index.ts"
compatibility_date = "2023-04-30"

[vars]
PINECONE_INDEX = "XXX"
src/index.ts
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アダプタが入るとそれを設定するだけに済みそうです。ロードマップ不明ですがプルリクエストはあります。

https://github.com/axios/axios/pull/5146

Tips(2): 古いバージョンのPineconeクライアントを使う

最新版のPineconeクライアントはNode.jsのprocess変数に依存しているのでCloudflare Workersでは動作しません。

この問題もミニマムなAPIクライアントを自作するのが本来はよいのですが、コミュニティではprocessが入る前のバージョンを使うワークアラウンドが知られているみたいです。

https://github.com/pinecone-io/pinecone-ts-client/issues/44

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のブログで公開されています。

https://supabase.com/blog/openai-embeddings-postgres-vector

僕もSupabaseとCloudflare Workersの組合せで作っても良かったのですが、記事の想定読者はフロントエンドエンジニアなのでベクトルDBサービスの部分を外部化しやすいPineconeを選びました。

またTraining ChatGPT with Custom Libraries Using ExtensionsではRubyとRedisのRediSearch拡張による類似度ベクトル検索の構築が紹介されています。

https://release.com/blog/training-chatgpt-with-custom-libraries-using-extensions

Cloudflare Workers以外の選択肢

Cloudflare Workersではなく通常のNode.jsを動作環境にする場合はベクターDBの選択肢が増えます。

Node.jsでnoteアカウントの記事を参照してGPTに質問するではhnswlibを使い、ローカルにインデックスファイルを書き出す設計にしました。

https://zenn.dev/laiso/articles/faa984a3e33e2b

Discussion