Craft Vector Search を利用する
Craft Vector Search(ベクターサーチ)はデータの検索に利用できるベクトルデータベースを提供します。ベクトルデータベースは、テキストや画像といったデータを数値列(ベクトル)形式でインデックス化して格納したもので、高速かつ、文脈を捉えた検索をする際に利用できます。
ベクターサーチの概要
ベクターサーチでは、ベクトルデータを保存するデータベースをインデックスと呼びます。インデックスに検索対象のベクトルデータを保存することで、データの検索が実現できます。
インデックスにデータを保存する方法として、現状ストリーム更新のみ対応しています。
- ストリーム更新
- ベクトルデータを1つずつ保存する方式です。
- Craft Functionsの
vectorSearch
モジュールを使ってデータを更新します。
保存したデータの検索はCraft Functionsの vectorSearch
モジュールを使って行います。
以上をまとめると、ベクターサーチを使うために必要な作業は次のようになります。
- インデックスを作成する
- インデックスの作成は管理画面の操作で行います。
- ベクトルデータを保存・更新する
- Craft Functionsを使ってストリーム更新します。
- Craft Functions でデータを検索する
- データを検索するファンクションを作成します。
インデックスを作成・削除する
インデックスの作成および削除手順を説明します。
インデックスを作成する
新たにインデックスを作成する手順は次のとおりです。
- KARTE管理画面で「すべてのメニュー」>「Craft」>「ベクターサーチ」を選択し、ベクターサーチの管理画面を開きます。
- 「新規作成」を選択し、インデックスの作成画面を開きます。
- 「名前」 にインデックス名を入力します。
- 「次元数」 で保存するベクトルの次元数を指定します。
- 「更新方法」 でインデックスの更新方法を指定します。
- 「作成」を選択します。
- インデックスの作成処理が開始します。
- しばらく待機後に管理画面を更新すると、インデックスの作成状況が現れます。「ステータス」がActiveになればインデックスが利用できるようになります。
インデックスを削除する
作成したインデックスを削除する手順は次のとおりです。
- KARTE管理画面で「すべてのメニュー」>「Craft」>「ベクターサーチ」を選択し、ベクターサーチの管理画面を開きます。
- 対象インデックスの三点リーダー(…)から「設定」を選択します。
- 「(インデックス名)を削除しますか?」というメッセージが表示されるので、「削除」を選択します。
- インデックスの削除処理が開始します。
- しばらく待機するとインデックスが削除されます。管理画面を更新してインデックスが消えていれば削除完了です。
インデックスにベクトルデータを取り込む
ベクトルデータを保存する方法を説明します。
データの埋め込み
データをベクトルに変換する処理を埋め込み(Embedding)と呼びます。埋め込み処理には、Craft AI Modulesを利用できます。
Craft AI Modules Google | KARTE Craft Developer Portal
また、各社が提供しているEmbedding APIも利用可能です。
- Embeddings API の概要 | Generative AI on Vertex AI | Google Cloud
- Azure OpenAI Service を使用して埋め込みを生成する方法 - Azure OpenAI | Microsoft Learn
- Amazon Titan Text Embeddings モデル - Amazon Bedrock
- Vector embeddings - OpenAI API
Craft AI Modules を使って埋め込みを行う
Craft AI Modules を使うことで、Google Cloud Vertex AIのEmbedding APIが利用できます。
サンプルコードを示します。次のコードは、HTTPタイプのファンクションとして動作します。POSTリクエストを受け取って、文字列をベクトルデータに変換します。
// 認証用のトークン。変数 TOKEN に適当な値を指定してください。const TOKEN = "<% TOKEN %>";// ログレベル。DEBUG, INFO, WARN, ERROR が指定できます。const LOG_LEVEL = "<% LOG_LEVEL %>";
export default async function (data, { MODULES }) { // リクエストとレスポンスのオブジェクトを取得 const { res, req } = data;
// リクエストメソッドがPOSTでない場合、405エラーを返す if (req.method !== "POST") { res.status(405).json({ error: "Method Not Allowed" }); return; }
// リクエストボディからcontentを取得 const { content } = req.body; const { aiModules, initLogger } = MODULES; // ロガーを初期化 const logger = initLogger({ logLevel: LOG_LEVEL });
// リクエストヘッダーを取得 const headers = req.headers;
// Content-Typeヘッダーがない場合、400エラーを返す if (!headers["content-type"]) { res.status(400).json({ error: "Content-Type header is missing" }); return; }
// Authorizationヘッダーからトークンを取得 const token = req.headers.authorization?.split(" ")[1]; // トークンがない場合、警告をログに記録し、400エラーを返す if (!token) { logger.warn("Missing 'token' parameter"); return res.status(400).json({ error: "Missing 'token' parameter" }); } // トークンが無効な場合、警告をログに記録し、401エラーを返す if (token !== TOKEN) { logger.warn("Invalid token"); return res.status(401).json({ error: "Invalid token" }); }
try { // Craft AI Modules を使用してテキストの埋め込みを取得 const { predictions } = await aiModules.gcpEmbeddingsText({ text: content, }); const r = predictions[0]; const { embeddings } = r; // 結果をログに記録 logger.log(r); // 埋め込みをレスポンスとして返す res.status(200).json({ embeddings }); } catch (e) { // エラーが発生した場合、エラーログを記録し、500エラーを返す logger.error(e); res.status(500).json({ error: "Failed to retrieve data" }); }}
このファンクションにリクエストを行うCurlコマンドの例は次のとおりです。
curl 'https://FUNCTION_URL' \-H 'Content-Type: application/json' \-H 'Authorization: Bearer TOKEN' \-d '{ "content": "こんにちは"}'
FUNCTION_URL
はファンクションのエンドポイントURLです。- Authorizationヘッダーの
TOKEN
には、変数TOKEN
の値を指定します。 - リクエストボディ (
-d
オプションの値) には次の値を指定します。content
: ベクトルデータに変換する文字列
ストリーム更新でベクトルデータを保存する
ストリーム更新でベクトルデータを保存する際には、Craft Functionsのファンクションを使います。
ベクトルデータを追加保存する
ベクトルデータの保存には、モジュール Modules.vectorSearch
を使います。次のコードは、POSTリクエストで受け取ったベクトルデータを特定のインデックスに書き込むHTTPタイプのファンクションのコードです。
// 認証用のトークン。変数 TOKEN に適当な値を指定してください。const TOKEN = "<% TOKEN %>";// インデックスのIDを変数 INDEX_ID に指定してください。const indexId = "<% INDEX_ID %>";// ログレベル。DEBUG, INFO, WARN, ERROR が指定できます。const LOG_LEVEL = "<% LOG_LEVEL %>";
export default async function (data, { MODULES }) { const { res, req } = data;
if (req.method !== "POST") { res.status(405).json({ error: "Method Not Allowed" }); return; }
const { datapoint_id, // ベクトルデータのキー feature_vector, // ベクトルデータの本体 categories, // カテゴリ検索用の値 } = req.body; const { vectorSearch, initLogger } = MODULES; const logger = initLogger({ logLevel: LOG_LEVEL });
const headers = req.headers; // Content-Type ヘッダーをチェックする if (!headers["content-type"]) { res.status(400).json({ error: "Content-Type header is missing" }); return; }
// Authorization ヘッダーで簡易な認証を行う const token = req.headers.authorization?.split(" ")[1]; if (!token) { logger.warn("Missing 'token' parameter"); return res.status(400).json({ error: "Missing 'token' parameter" }); } if (token !== TOKEN) { logger.warn("Invalid token"); return res.status(401).json({ error: "Invalid token" }); }
try { // パラメータの存在チェック if (!datapoint_id || !feature_vector) { res.status(400).json({ error: "Missing required parameters for upsert" }); return; }
// カテゴリ検索用の制約を定義する。 const restricts = [ { namespace: "categories", allow_list: [...categories], deny_list: [], }, ]; const datapoints = [{ datapoint_id, feature_vector, restricts }];
// ベクターサーチのインデックスにレコードを書き込む await vectorSearch.upsert({ indexId, datapoints });
res.json({ datapoint_id }); } catch (e) { logger.error(e); res.status(500).json({ error: "Failed to upsert data" }); }}
このファンクションにリクエストを行うCurlコマンドの例は次のとおりです。
curl 'https://FUNCTION_URL' \-H 'Content-Type: application/json' \-H 'Authorization: Bearer TOKEN' \-d '{ "datapoint_id": "product_001", "feature_vector": [0.123, 0.456, 0.789, 0.321 ... ], "categories": ["electronics", "smartphone"]}'
FUNCTION_URL
はファンクションのエンドポイントURLです。- Authorizationヘッダーの
TOKEN
には、変数TOKEN
の値を指定します。 - リクエストボディ (
-d
オプションの値) には次の値を指定します。datapoint_id
: ベクトルデータを識別するユニークなIDfeature_vector
: 実際のベクトルデータ(数値の配列)- ベクターサーチで指定した次元数と同じ要素数を指定します。
categories
: カテゴリ検索用の文字列配列
このコードでは、次の処理を行っています。
- 受信したHTTPリクエストの検証
- 認証用のAuthorizationヘッダーの検証
- Content-Typeヘッダーの検証
- Bodyに入っているパラメータの検証
- ベクターサーチのインデックスへのデータ書き込み
- リクエストBodyのパラメータからベクトル埋め込みを生成します。
- 指定したインデックスIDに対して、
vectorSearch.upsert()
を使用してベクトルデータとメタデータを格納する。
- エラーハンドリング
- リクエスト検証に失敗した場合、エラーレスポンスを返却します。
- データの書き込みに失敗した場合も、エラーレスポンスを返却します。
ベクトルデータを削除する
モジュール MODULES.vectorSearch
はインデックスからレコードを削除する機能も持っています。次のコードは、POSTリクエストで受け取った datapoint_id
に基づいてインデックスからデータを削除するHTTPタイプのファンクションのコードです。
// 認証用のトークン。変数 TOKEN に適当な値を指定してください。const TOKEN = "<% TOKEN %>";// インデックスのIDを変数 INDEX_ID に指定してください。const indexId = "<% INDEX_ID %>";// ログレベル。DEBUG, INFO, WARN, ERROR が指定できます。const LOG_LEVEL = "<% LOG_LEVEL %>";
export default async function (data, { MODULES }) { const { res, req } = data;
if (req.method !== "POST") { res.status(405).json({ error: "Method Not Allowed" }); return; }
const { datapoint_id, // ベクトルデータのキー partition, // パーティション(省略可) } = req.body; const { vectorSearch, initLogger } = MODULES; const logger = initLogger({ logLevel: LOG_LEVEL });
const headers = req.headers; // Content-Type ヘッダーをチェックする if (!headers["content-type"]) { res.status(400).json({ error: "Content-Type header is missing" }); return; }
// Authorization ヘッダーで簡易な認証を行う const token = req.headers.authorization?.split(" ")[1]; if (!token) { logger.warn("Missing 'token' parameter"); return res.status(400).json({ error: "Missing 'token' parameter" }); } if (token !== TOKEN) { logger.warn("Invalid token"); return res.status(401).json({ error: "Invalid token" }); }
try { const datapointIds = [datapoint_id]; await vectorSearch.remove({ indexId, datapointIds, partition }); res.json({ datapoint_id }); return; } catch (e) { logger.error(e); res.status(500).json({ error: "Failed to remove data" }); }}
このファンクションにリクエストを行うCurlコマンドの例は次のとおりです。
curl 'https://FUNCTION_URL' \-H 'Content-Type: application/json' \-H 'Authorization: Bearer TOKEN' \-d '{ "datapoint_id": "product_001", "partition": "app-1"}'
FUNCTION_URL
はファンクションのエンドポイントURLです。- Authorizationヘッダーの
TOKEN
には、変数TOKEN
の値を指定します。 - リクエストボディ (
-d
オプションの値) には次の値を指定します。datapoint_id
: ベクトルデータを識別するユニークなIDpartition
: データの論理的な分割(省略可)。upsert時に指定したpartitionと同じ値を指定してください。
このコードでは、次の処理を行っています。
- 受信したHTTPリクエストの検証
- 認証用のAuthorizationヘッダーを検証する。
- Content-Typeヘッダーを検証する。
- Bodyに入っているパラメータを検証する。
- ベクターサーチのインデックスからのデータ削除
- インデックスIDとリクエストで受け取ったdatapoint_idを使用して、
vectorSearch.remove()
でベクトルデータを削除します。
- インデックスIDとリクエストで受け取ったdatapoint_idを使用して、
- エラーハンドリング
- リクエスト検証に失敗した場合、エラーレスポンスを返却します。
- データの削除に失敗した場合も、エラーレスポンスを返却します。
データを検索する
ベクターサーチを使ってデータを検索する方法をサンプルコードを交えて説明します。
データの埋め込み
ベクターサーチでデータを検索する際には、検索クエリをベクトルデータに変換してください。各社が提供している埋め込みAPI (Embedding API) を利用します。なお、インデックスへのデータ更新と検索では、同一のEmbedding APIを利用してください。
データの検索
データの検索は、Craft Functionsのファンクションで行います。ストリーム更新と同じく、モジュール MODULES.vectorSearch
を使います。
次のコードは、HTTPタイプのファンクションで検索クエリとなるベクトルデータをPOSTリクエストで受け取り、インデックスを検索して上位5件のベクトルのdatapoint_idを返すコードの例です。
// 認証用のトークン。変数 TOKEN に適当な値を指定してください。const TOKEN = "<% TOKEN %>";// エンドポイントIDを変数 INDEX_ENDPOINT_ID に指定してください。const indexEndpointId = "<% INDEX_ENDPOINT_ID %>";// ログレベル。DEBUG, INFO, WARN, ERROR が指定できます。const LOG_LEVEL = "<% LOG_LEVEL %>";
export default async function (data, { MODULES }) { const { res, req } = data;
if (req.method !== "POST") { res.status(405).json({ error: "Method Not Allowed" }); return; }
const { feature_vector, // ベクトルデータの本体 category, // カテゴリ検索のキー (更新時に categories で指定した値のみ有効) } = req.body; const { vectorSearch, initLogger } = MODULES; const logger = initLogger({ logLevel: LOG_LEVEL });
const headers = req.headers; // Content-Type ヘッダーをチェックする if (!headers["content-type"]) { res.status(400).json({ error: "Content-Type header is missing" }); return; }
// Authorization ヘッダーで簡易な認証を行う const token = req.headers.authorization?.split(" ")[1]; if (!token) { logger.warn("Missing 'token' parameter"); return res.status(400).json({ error: "Missing 'token' parameter" }); } if (token !== TOKEN) { logger.warn("Invalid token"); return res.status(401).json({ error: "Invalid token" }); }
// ベクトルデータで検索を行う try { // categories による絞り込み情報を設定する const restricts = [ { namespace: "categories", allow_list: [category], deny_list: [], }, ];
// 検索クエリを構成する const queries = [ { datapoint: { feature_vector, restricts }, neighborCount: 5 }, ]; // インデックス内を検索する const { nearestNeighbors } = await vectorSearch.findNeighbors({ indexEndpointId, queries, });
// 最短近傍のデータが存在しない場合はサーバーエラーを返す if (nearestNeighbors == null) { res.status(500).json({ error: "Failed to find nearest neighbors" }); return; }
// データが存在する場合は、datapoint の情報と距離を返す const results = nearestNeighbors[0].neighbors.map((n) => { const { distance, datapoint } = n; return { distance, datapoint }; });
// 結果をログに出力し、レスポンスとして返却する logger.log({ results }); res.json({ results }); } catch (e) { // エラーが発生した場合はエラーログを記録して、サーバーエラーを返す logger.error(e); res.status(500).json({ error: "Failed to retrieve data" }); }}
このファンクションにリクエストを行うCurlコマンドの例は次のとおりです。
curl 'https://FUNCTION_URL' \-H 'Content-Type: application/json' \-H 'Authorization: Bearer TOKEN' \-d '{ "feature_vector": [0.123, 0.456, 0.789, 0.321 ... ], "category": "smartphone"}'
FUNCTION_URL
はファンクションのエンドポイントURLです。- Authorizationヘッダーの
TOKEN
には、変数TOKEN
の値を指定します。 - リクエストボディ (
-d
オプションの値) には次の値を指定します。feature_vector
: 実際のベクトルデータ(数値の配列)- ベクターサーチで指定した次元数と同じ要素数を指定します。
categories
: カテゴリ検索用の文字列配列
このコードでは、次の処理を行っています。
- 受信したHTTPリクエストの検証
- 認証用のAuthorizationヘッダーを検証する。
- Content-Typeヘッダーを検証する。
- Bodyに入っているパラメータを検証する。
- ベクターサーチのインデックスでの検索
- リクエストで受け取ったベクトルデータと類似する上位5件のベクトルデータを検索する。
- カテゴリによる絞り込みがある場合は、そのカテゴリに属するデータの中から検索する。
- エラーハンドリング
- リクエスト検証に失敗した場合、エラーレスポンスを返却する。
- ベクトルサーチでの検索に失敗した場合も、エラーレスポンスを返却する。
ベクターサーチのインデックスはベクトル化したデータしか保持していませんが、実用上はベクトルデータに変換する前のコンテンツ情報が必要になるケースがあります。その場合はdatapoint_idをキーに別のデータソースを引くように実装してください。
Partition を使って論理的にデータを分離する
同一のインデックスを複数アプリで共有する場合など、レコードを論理的に分離したいときは partition
を指定します。partition
を渡すと、内部で restricts
に namespace: "__CRAFT_INTERNAL_PARTITION__", allow_list: [partition]
が自動付与されます。
更新(upsert)での指定例
await vectorSearch.upsert({ indexId, datapoints: [ { datapoint_id: "point-1", feature_vector: [0.1, 0.2, 0.3], }, ], partition: "app-1",});
検索(findNeighbors)での指定例
const { nearestNeighbors } = await vectorSearch.findNeighbors({ indexEndpointId, queries: [ { datapoint: { feature_vector }, neighborCount: 5, }, ], partition: "app-1",});
注意事項は次のとおりです。
partition
を省略した場合は自動付与されません(全データ対象)。- 既存の
datapoints[*].restricts
やqueries[*].datapoint.restricts
がある場合でも、partition
によるrestrictsは追加マージされます。 partition
を指定した場合、同じdatapoint_id
でも異なるpartition
であれば別のデータとして扱われます。削除(remove
/removeWithKvs
)の際も、upsert時に指定したpartitionと同じ値を指定します。
WithKvs を使ってデータを保存・取得する
ベクター(特徴量)はベクトルインデックスへ、付随するデータはKVSに保存し、検索結果に自動付与できます。
なお、KVSのminutesToExpireの上限は、通常のプランでは現在時刻から1 month(44,640min)までです。これを超える値を指定したい場合は、別途プレイドの営業・カスタマーサクセスにお問い合わせください。
追加・更新(upsertWithKvs)
await vectorSearch.upsertWithKvs({ indexId, datapoints: [ { datapoint_id: "point-1", feature_vector: [0.1, 0.2, 0.3], data: { title: "商品A", price: 1200 }, // KVS に保存されるメタデータ }, ], kvsMinutesToExpire: 60, // 省略可 partition: "app-1", // 省略可});
削除(removeWithKvs)
await vectorSearch.removeWithKvs({ indexId, datapointIds: ["point-1"], partition: "app-1", // 省略可。upsert時に指定したpartitionと同じ値を指定してください。});
検索(findNeighborsWithKvs)
const { nearestNeighbors } = await vectorSearch.findNeighborsWithKvs({ indexEndpointId, queries: [ { datapoint: { feature_vector }, neighborCount: 5, }, ], partition: "app-1",});
// 返却例: neighbors[*].data に upsert 時の data が付与されますconst results = nearestNeighbors[0].neighbors.map((n) => ({ id: n.datapoint.datapointId, distance: n.distance, data: n.data, // { title: "商品A", price: 1200 }}));