コンテンツにスキップ

Craft Vector Search を利用する

Craft Vector Search(ベクターサーチ)はデータの検索に利用できるベクトルデータベースを提供します。ベクトルデータベースは、テキストや画像といったデータを数値列(ベクトル)形式でインデックス化して格納したもので、高速かつ、文脈を捉えた検索をする際に利用できます。

ベクターサーチの概要

ベクターサーチでは、ベクトルデータを保存するデータベースをインデックスと呼びます。インデックスに検索対象のベクトルデータを保存することで、データの検索が実現できます。

インデックスにデータを保存する方法として、現状ストリーム更新のみ対応しています。

  • ストリーム更新
    • ベクトルデータを1つずつ保存する方式です。
    • Craft Functionsの vectorSearch モジュールを使ってデータを更新します。

保存したデータの検索はCraft Functionsの vectorSearch モジュールを使って行います。

以上をまとめると、ベクターサーチを使うために必要な作業は次のようになります。

  • インデックスを作成する
    • インデックスの作成は管理画面の操作で行います。
  • ベクトルデータを保存・更新する
    • Craft Functionsを使ってストリーム更新します。
  • Craft Functions でデータを検索する
    • データを検索するファンクションを作成します。

インデックスを作成・削除する

インデックスの作成および削除手順を説明します。

インデックスを作成する

新たにインデックスを作成する手順は次のとおりです。

  1. KARTE管理画面で「すべてのメニュー」>「Craft」>「ベクターサーチ」を選択し、ベクターサーチの管理画面を開きます。
  2. 「新規作成」を選択し、インデックスの作成画面を開きます。
    1. 「名前」 にインデックス名を入力します。
    2. 「次元数」 で保存するベクトルの次元数を指定します。
    3. 「更新方法」 でインデックスの更新方法を指定します。
  3. 「作成」を選択します。
  4. インデックスの作成処理が開始します。
  5. しばらく待機後に管理画面を更新すると、インデックスの作成状況が現れます。「ステータス」がActiveになればインデックスが利用できるようになります。

インデックスを削除する

作成したインデックスを削除する手順は次のとおりです。

  1. KARTE管理画面で「すべてのメニュー」>「Craft」>「ベクターサーチ」を選択し、ベクターサーチの管理画面を開きます。
  2. 対象インデックスの三点リーダー(…)から「設定」を選択します。
  3. 「(インデックス名)を削除しますか?」というメッセージが表示されるので、「削除」を選択します。
  4. インデックスの削除処理が開始します。
  5. しばらく待機するとインデックスが削除されます。管理画面を更新してインデックスが消えていれば削除完了です。

インデックスにベクトルデータを取り込む

ベクトルデータを保存する方法を説明します。

データの埋め込み

データをベクトルに変換する処理を埋め込み(Embedding)と呼びます。埋め込み処理には、Craft AI Modulesを利用できます。

Craft AI Modules Google | KARTE Craft Developer Portal

また、各社が提供しているEmbedding 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: ベクトルデータを識別するユニークなID
    • feature_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: ベクトルデータを識別するユニークなID
    • partition: データの論理的な分割(省略可)。upsert時に指定したpartitionと同じ値を指定してください。

このコードでは、次の処理を行っています。

  • 受信したHTTPリクエストの検証
    • 認証用のAuthorizationヘッダーを検証する。
    • Content-Typeヘッダーを検証する。
    • Bodyに入っているパラメータを検証する。
  • ベクターサーチのインデックスからのデータ削除
    • インデックスIDとリクエストで受け取ったdatapoint_idを使用して、vectorSearch.remove()でベクトルデータを削除します。
  • エラーハンドリング
    • リクエスト検証に失敗した場合、エラーレスポンスを返却します。
    • データの削除に失敗した場合も、エラーレスポンスを返却します。

データを検索する

ベクターサーチを使ってデータを検索する方法をサンプルコードを交えて説明します。

データの埋め込み

ベクターサーチでデータを検索する際には、検索クエリをベクトルデータに変換してください。各社が提供している埋め込み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 を渡すと、内部で restrictsnamespace: "__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[*].restrictsqueries[*].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 }
}));